Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Replace Providers by objects #3686

Closed
fzaninotto opened this issue Sep 12, 2019 · 5 comments
Closed

[RFC] Replace Providers by objects #3686

fzaninotto opened this issue Sep 12, 2019 · 5 comments

Comments

@fzaninotto
Copy link
Member

fzaninotto commented Sep 12, 2019

Problems

It's impossible to give useful Typescript types to the dataProvider, authProvider, and (since #3685) i18nProvider because they are functions accepting a type parameter as first argument, and with a return value depending on that type. This makes writing custom providers a bit harder.

Besides, the switch/case syntax of providers is sometimes seen as a bad coding practice.

Lastly, all the constants we use (like GET_ONE, AUTH_LOGOUT, etc) make the code heavier and longer to write.

Proposal

React-admin calls the Provider functions with a message type and arguments. There is another way to do that, more idiomatic to JavaScript: calling methods on an object.

I propose to transform the syntax of all 3 providers so that they can be objects.

For instance, an authProvider written as a function:

import { AUTH_LOGIN, AUTH_LOGOUT, AUTH_CHECK, AUTH_ERROR } from 'react-admin';

export default (type, params) => {
    if (type === AUTH_LOGIN) {
        const { username } = params;
        localStorage.setItem('username', username);
        // accept all username/password combinations
        return Promise.resolve();
    }
    if (type === AUTH_LOGOUT) {
        localStorage.removeItem('username');
        return Promise.resolve();
    }
    if (type === AUTH_ERROR) {
        return Promise.resolve();
    }
    if (type === AUTH_CHECK) {
        return localStorage.getItem('username')
            ? Promise.resolve()
            : Promise.reject();
    }
    return Promise.reject('Unkown method');
};

Can be written as an object as follows:

export default {
    login({ username }) {
        localStorage.setItem('username', username);
        // accept all username/password combinations
        return Promise.resolve();
    },
    logout() {
        localStorage.removeItem('username');
        return Promise.resolve();
    },
    error() {
        return Promise.resolve();
    },
    check() {
        return localStorage.getItem('username')
            ? Promise.resolve()
            : Promise.reject();
    }
}

This is very easy to Type using TypeScript. Something like:

type AuthProvider {
    login: (args: any) => Promise<void>;
    logout: () => Promise<void>;
    error: (error: any) => Promise<void>;
    check: () => Promise<void>
}

Backward Compatibility

This is not a breaking change if react-admin detects that a provider is a function rather than an object, and wraps it inside a converter function:

import { AUTH_LOGIN, AUTH_LOGOUT, AUTH_ERROR, AUTH_CHECK } from 'react-admin';

const convertFunctionAuthProvider = authProvider => ({
    login: args => authProvider(AUTH_LOGIN, args),
    logout: () => authProvider(AUTH_LOGOUT),
    error: error => authProvider(AUTH_LOGOUT, error),
    check: ()=> authProvider(AUTH_CHECK)
})

So we can make it painless for the users, and preserve the large list of available (data-) providers.

Notes

We've been using composition as an argument for using functions for providers. For instance, to use two different providers for two sets of resources:

const userProvider = simpleRestProvider('http://my.domain.com/users'); // (type, resource, payload => Promise
const postProvider = simpleRestProvider('http://my.other.domain.com/posts'); // (type, resource, payload => Promise

const dataProvider = (type, resource, payload) => {
    if (resource === 'users') return userProvider(type, resource, payload);
    if (resource === 'posts') return postProvider(type, resource, payload);
}

This is very practical, but nothing you can't do with objects:

type DataProvider {
    getList: (resource: string, params: any) => Promise<Record[]>;
    getOne: (resource: string, id: Identifier) => Promise<Record>;
   // ... 
}

const userProvider = simpleRestProvider('http://my.domain.com/users'); // DataProvider
const postProvider = simpleRestProvider('http://my.other.domain.com/posts'); // DataProvider

const dataProvider = {
   __noSuchMethod__: (id, args) => {
        if (args[1] === 'users') return userProvider[id].apply(userProvider, args),
        if (args[1] === 'posts') return postProvider[id].apply(postProvider, args),
    }
}

What's your take?

I may forget some pros and cons, please comment on this issue to give your opinion!

@fzaninotto
Copy link
Member Author

I forgot #3340: because they are functions, our providers aren't really practical to use in a useState() call... Turning them to objects would remove that gotcha (that I encountered myself).

@fzaninotto
Copy link
Member Author

A con I didn't think of: there is an upgrade cost for users, not when injecting providers, but when using them.

We can make it so there is no cost when using dataProvider through <Query> or <Mutation>:

import { Query } from 'react-admin';

const UserProfile = ({ record }) => (
    <Query type="GET_ONE" resource="users" payload={{ id: record.id }}>
        {({ data, loading, error }) => {
            if (loading) { return <Loading />; }
            if (error) { return <p>ERROR</p>; }
            return <div>User {data.username}</div>;
        }}
    </Query>
);

To make that work, we'll need to support both the GET_ONE and the getOne types. Not a big deal, and zero work for the end user.

But users will have to make changes to their code using withDataProvider:

// in src/comments/ApproveButton.js
import {
   showNotification,
   UPDATE,
   withDataProvider,
} from 'react-admin';

class ApproveButton extends Component {
    handleClick = () => {
        const { dataProvider, dispatch, record } = this.props;
        const updatedRecord = { ...record, is_approved: true };
-       dataProvider(UPDATE, 'comments', { id: record.id, data: updatedRecord })
+       dataProvider.update('comments', { id: record.id, data: updatedRecord })
            .then(() => {
                dispatch(showNotification('Comment approved'));
                dispatch(push('/comments'));
            })
            .catch((e) => {
                dispatch(showNotification('Error: comment not approved', 'warning'))
            });
    }

    render() {
        return <Button label="Approve" onClick={this.handleClick} />;
    }
}

ApproveButton.propTypes = {
    dataProvider: PropTypes.func.isRequired,
    dispatch: PropTypes.func.isRequired,
    record: PropTypes.object,
};

export default withDataProvider(ApproveButton)

I'm afraid that the cost of migration reduces the benefit/cost ratio.

That, and the fact that starting such a refactoring will delay the release of v3 by at least a couple weeks.

@fzaninotto
Copy link
Member Author

For people who don't use TypeScript (and that's 100% of people since we don't publish types yet), this is just cost with no benefit... So I tend to think this is premature. We should only make that change when react-admin types are published, which will not be in v3.0 (too much work).

@fzaninotto
Copy link
Member Author

fzaninotto commented Sep 15, 2019

I had a revelation while jogging in the woods with my dog: This change can be made in a totally backward-compatible way, because in JavaScript, functions are also objects.

So if developers pass a function provider to Admin, instead of replacing that function by an object provider, we can dynamically add methods to the function, calling the function itself. Something like:

const convertFunctionAuthProvider = authProvider => {
    authProvider.login = args => authProvider(AUTH_LOGIN, args);
    authProvider.logout = () => authProvider(AUTH_LOGOUT);
    authProvider.error = error => authProvider(AUTH_LOGOUT, error);
    authProvider.check = ()=> authProvider(AUTH_CHECK);
    return authProvider;
};

That way, both the internal react-admin calls (using methods like login) and legacy code by the developers (using function call with constant verbs) will work.

This change is therefore a net benefit with no cost for the developers. I'm +1 for incorporating it in v3.

@fzaninotto
Copy link
Member Author

We've decided to do it, so I'm closing this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant