bitovi / ylem

Add Observable View-Models to React components
https://bitovi.github.io/ylem/
MIT License
42 stars 2 forks source link

Proposal: Support Hooks #191

Open christopherjbaker opened 5 years ago

christopherjbaker commented 5 years ago

Hooks is direction that React is going, regardless of any issues that some of us might have with them.

The API inspiration for our current project comes from recompose and redux; the recompose library is being discontinued in favor of hooks, and one of the primary redux developers was on the hooks project in order create redux-replacement hooks like useReducer. While redux is not being discontinued, it is no longer necessary. For this reason, I propose that we release ylem v3 that primarily supports hooks, though old apis are maintained and decprecated at ylem/legacy.

After reviewing how hooks work, I think doing this would greatly simplify the code base, as we are no longer mucking with React internals. It might be possible to support a similar api as before, though it would remove a lot of these improvements.

Ylem will only have a future if it supports hooks. To that end, there are two decisions that need to be made regarding hook implementation.

Providing the Store

The Store class must be provided to the hook in some manner. This can either be a 1 step process or a 2 step process:

1 step

The benefit here is simplicity: It is a single line.

function Foo() {
 const store = useYlemStore(Store);

 return (<div>{store.name}</div>);
}

2 step

This separates the above line into two line. The first could be in the component, or it could be exported by the Store file itself (asa import { useStore } from './store'). In that case, the benefit is that all ylem-specific code is nowhere near the component; the component just gets data from a hook and doesn't need to know how it works.

const useStore = createStoreHook(Store);

function Foo() {
 const store = useStore();

 return (<div>{store.name}</div>);
}

Providing the Data

Data will need to be provided to ylem, for initializing and updating the store; this will most likely come from props. For simplicity, I will assume option 2 above.

props

I propose that the primary method is for the hook to take props. In lieu of other configuration, the props would be used for initializing the store and changes to props will be diffed and those patches will be applied to the store.

function Foo(props) {
 const store = useStore(props);

 return (<div>{store.name}</div>);
}

As is the standard with useEffect and other hooks, we should accept a second argument that is used for validating if the store needs to be updated. This can simply be proxied to the underlying useEffect call, so it should require very little work on our end.

function Foo(props) {
 const store = useStore(props, [ props.name ]);

 return (<div>{store.name}</div>);
}

Function

I propose that the same hook can also take a function. This function would be called with the current store instance. This function can do some combination of three things:

  1. Modify the store instance. const store = useStore((store) => { store.name = props.name; })
  2. Return a plain object, which will be diffed and patched as if that object were passed directly (see above section). const store = useStore(() => ({ data: props }))
  3. Return a promise that resolves to a plain object, it will behave as the object does above. This option will allow us to tie into react's upcoming suspense feature.

Additional Pieces

Observing without creating a store

Whether or not this is exposed to user-land, there will be a hook which just handles the observing/rerendering. This takes no data and returns no data. Is there a use case for exposing this?

function Foo() {
 useYlem();
 return (<div>Hello</div>);
}

Model Provider

Many react projects make use of providers, allow some piece of information to be defined at a level higher than it is consumed. One possibility in our case is to have a ModelProvider.

It would look something like this, allowing one to use a string in place of a store constructor. This would work with either method of providing the store constructor.

function Foo() {
 return (
    <ModelProvider MyStore={MyStore}>
      <Bar />
    </ModelProvider>
  );
}

function Bar() {
 const store = useYlemStore('MyStore');

 return (<div>{store.name}</div>);
}
function Bar() {
 const Store = useModel('MyStore');
 // do something with Store

 return (<div>{store.name}</div>);
}
BigAB commented 5 years ago

Some comments about above:

which may become outdated if you edit the above


In the Providing the Data -> function section, points3 and 4 seem odd and out of place as written.

Now because I helped come up with these ideas, I know why they are there, and why they are important, but just tacking them on to the Providing the Data section is confusing, they probably require their own section and explanation with some examples.


Observing without creating a store

Is there a use case for exposing this?

The use case is the same as the previous ylems @observe decorator. If you pass an observable store instance down into some "deeper" react components it could possibly offer a performance boost by tying the "deep component" to an observer, so that changes may only cause the "deep component" to re-render, not the component with the store instance created with the hook (because we set up an observer in the useYlemStore).

This would probably be worth having, and documenting as a solution to like the "only update one item in a list" problem.


Model Provider

This could be an issue of its own. It's not really limited to hooks, though more attractive with the hook usage, and it could have more details and examples of usage.


Hooks only?

Honestly I don't see a problem keeping our v2 code in, especially if it can be tree shaken, and allow the old usage alongside of hooks, but lets deprecate the Class, HoC and Render Prop stuff and just say "You should switch to hooks". And give limited support to the old features.

Hooks are the bomb, this needs to be done sooner than later.

mjstahl commented 5 years ago

I like the two step approach (separation of create and use) primarily because I like the flexibility to test the store separately from the view.

The rest I know basically 0 about, so I will leave that up to better individuals than I.

Nice job.

christopherjbaker commented 5 years ago

@mjstahl You can test the store independently of the view even with a 1 step process. You can't test the hook directly like that, but you wouldn't need to, since ylem tests the hooks.