Closed dustingetz closed 8 years ago
@danielmiladinov @meta-meta @lijunle What do you guys think?
I don't fully get your meaning? How will the usage syntax look like after this change?
Will it look same as immutable.js way?
Will user need to call setState by his own code?
It could be great to provide some usage snippets.
Thanks!
We'd stop using React state completely, all data is above the React root and passed down as props by React.render. The Cursor constructor would be passed a reference to state value, and a setState function. Those can either be a React cmp.state and cmp.setState, or we can pass in our own reference and our own setState function that mutates the reference with updated state (no react dependency). In the non-react case, we'd have to handle re-rendering and render batching ourselves, like in ClojureScript world. I don't know how people using immutablejs handle batching but presumably its similar.
That is awesome. Cursor is passed from React.render function and I do not need to specially treat root component and sub-component any more.
But, AFAIK, on the React.render level, there is no way to get setState function before invoke render function.
Such syntax?
var cursor = Cursor.build(data) // no way to get setState function here
var cmp = React.render(App, cursor, dom)
cursor.initSetState(cmp.setState)
To directly answer your question, here is one way: https://github.com/mquan/cortex/blob/master/examples/skyline/application.jsx#L107-L113
Another way
var state = { a: { b: 42 }}; // this reference owns the state, like a singleton
var cursor = Cursor.build(state);
React.render(<App cursor={cursor}/>, domEl)
cursor.onUpdates((cursor) => React.render(<App cursor={cursor}/>, domEl))
// onUpdates needs to implement batching strategy
@dustingetz Your sample code is awesome!
I have two issues.
onUpdate
method and rename it to something like initUpdate
React itself has already implement the batching strategy. I could prefer to leave these jobs for React and do nothing in cursor level.
I agree but I think this is incompatible with this approach? If I understand correctly, React batching strategy requires us use react setState. I might be wrong.
I search on the React source code, the answer is no.
The render
function find if the element is mounted. If yes, only update the changes. Inside update function, React push the changes to the queue.
So if I understand correctly, this sample code will batch properly with no extra code?
cursor.onUpdates((cursor) => React.render(<App cursor={cursor}/>, domEl))
I think so.
@dustingetz I purpose the usage syntax, and update the examples and unit tests. How do you think about it? https://github.com/dustingetz/react-cursor/compare/dustingetz:master...lijunle:v2
(Note: no production code changes, tests will fail.)
I'm still thinking about the best way to implement this. I'm currently thinking:
Two cursor types (scala syntax): ReactStateCursor(cmp: ReactComponent)
and PlainCursor(value: JsObject, swapper: JsObject => ())
. ReactStateCursor
is just PlainCursor(cmp.state, cmp.setState)
. This way I can retain backwards compatibility with existing code, because one design goal of react-cursor is to be fast and easy to integrate with largish React apps that are already using React state. Then an application can derive custom Cursor types as needed, like BatchingReactCursor, BatchingPlainCursor, ReactCursorWithPendingValue (#57) etc.
Are you meaning that, make some inheritance with various Cursor variants?
PlainCursor
/ \
/ \
ReactStateCursor (customized) MyPlainCursor
/ \
/ \
BatchingCursor PendingValueCursor
After that, PlainCursor and ReactStateCursor are shipped inside this package. Everybody can customize based on them.
In such case, how do you handle Cursor.build
API, wrap new ReactStateCursor
initialization?
Conceptually, yes. I would prefer composition to inheritance if possible. It's not yet clear to me the best way to handle cursor.build.
How about this?
Keep Cursor.build
as the current behavior for backward compatible (wrap new ReactStateCursor
). This is V1 usage style.
Document and recommend the V2 usage style:
const cursor = new Cursor({ ...initial state data... }); // or use other function, i.e., Cursor.create. We can discuss
cursor.init(() => React.render(<App cursor={cursor} />, dom));
Common users only use these two APIs - V1 style for Cursor.build
function, V2 style for Cursor constructor.
Build up with inheritance as described above, but document it as advanced features.
Currently, the only one API exposed is Cursor.build
function, I think such upgrade is smooth and easily understand.
Is v2 public api (new
to construct cursor) compatible with the memoizing strategy?
I am not sure, but I think it can be compatible.
I have this working locally:
var state = {
very: {
deeply: {
nested: {
counts: _.range(400).map(function () { return 0; })
}
}
}
};
function swapper(f) {
state = f(state);
queueRender();
}
function queueRender() {
var cur = ReactCursor.Cursor.build(state, swapper);
React.render(<App cursor={cur} />, document.getElementById('root'));
}
queueRender();
The latest api supports regular cursors (immutable value semantics for react lifecycle methods and shouldComponentUpdate) and also now RefCursors which are for business logic type code that only ever cares about the latest value and doesn't expose a notion of value equality since it is not in react lifecycle methods.
window.stateAtom = atom.createAtom({
very: {
deeply: {
nested: {
counts: _.range(400).map(function () { return 0; })
}
}
}
});
function queueRender(key, ref, prevVal, curVal) {
var cur = ReactCursor.Cursor.build(stateAtom.deref(), stateAtom.swap);
window.app = React.render(<App cursor={cur} />, document.getElementById('root'));
}
stateAtom.addWatch('react-renderer', queueRender);
queueRender('react-renderer', stateAtom, undefined, stateAtom.deref());
var cur = ReactCursor.Cursor.build(stateAtom.deref(), stateAtom.swap)
var refcur = ReactCursor.RefCursor.build(stateAtom.deref, stateAtom.swap)
cur.refine('very', 'deeply', 'nested', 'counts', 0).value // => 0
refcur.refine('very', 'deeply', 'nested', 'counts', 0).deref() // => 0
cur.refine('very', 'deeply', 'nested', 'counts', 0).set(100)
// Regular value cursors are not changed since immutable and have value semantics
cur.refine('very', 'deeply', 'nested', 'counts', 0).value // => 0
// Reference cursors see latest value
refcur.refine('very', 'deeply', 'nested', 'counts', 0).deref() // => 100
(This is not merged, see https://github.com/dustingetz/react-cursor/pull/63)
Fixed in 2.0-alpha.4
One real world issue we have run into, is that when React state is conceptually at the top, but actually it isn't at the top because there's non-react portions of the system above it, engines above it etc
As of React 13, cursor doesn't need to use a react private api anymore, because of the improved setState api. So Cursors really only depend on two things conceptually: a setState function, and a current state value. It doesn't actually have to be a React component anymore.
So given this, I believe I could implement the setState/value api outside of react and backed by regular javascript objects, thus cursors can be constructed above the React entry point. This is analogous to the model provided by other cursor implementations (ImmutableJS and Baobab), except it keeps the benefits of react-cursor over those approaches - namely, simplicity of code (a lot fewer lines of code), and simplicity of types (no special immutable types, just regular javascript objects)
Could be worth exploring.