Comments (14)
@jacob-ebey Yeah, my original motivation and implementation was also eliminating context, and then eventually I came back to use context which seems to work better in concurrent mode in the future. I also wanted to make use of observedBits
for performance.
from react-hooks-global-state.
Hi!
This is not exhaustive pros and cons, but let me explain.
If we go with the first API design, the dispatch
is globally available in a file and you can define action functions outside of components. You can even define them in a separate file and import them. We don't need mapDispatchToProps
technique like in react-redux.
const initialState = { counter: 0 }; // initialState is not optional.
const { GlobalStateProvider, dispatch, useGlobalState } = createStore(reducer, initialState);
const increment = () => dispatch({ type: 'increment' });
const decrement = () => dispatch({ type: 'decrement' });
const Counter = () => {
const [value] = useGlobalState('counter');
return (
<div>
<span>Counter: {value}</span>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
</div>
);
};
Not much different if use button
here, but suppose we use a custom MyButton
extending React.PureComponent or with React.memo(), we probably want onClick function identity.
To keep the function identity with the second API design, we need to use useCallback()
.
import { useCallback } from 'react';
const initialState = { counter: 0 }; // initialState is not optional.
const { GlobalStateProvider, useGlobalState } = createStore(reducer, initialState);
const Counter = () => {
const [value, dispatch] = useGlobalState('counter');
const increment = useCallback(() => dispatch({ type: 'increment' }), []);
const decrement = useCallback(() => dispatch({ type: 'decrement' }), []);
return (
<div>
<span>Counter: {value}</span>
<MyButton onClick={increment}>+1</MyButton>
<MyButton onClick={decrement}>-1</MyButton>
</div>
);
};
If developers are used to memoization, this is trivial, but it may still introduce boilerplate code, which I would like to avoid.
The downside of the global dispatch
is that you need to mock it when testing.
However, we still have useGlobalState
globally and probably it won't change much.
There might be other differences, and I'd admit that I need to learn more about testing components with hooks.
Historically, this library was developed primarily for the setState style.
const [value, update] = useGlobalState('counter');
So, the second value of useGlobalState
is an update function.
This is still true even if we use the reducer style.
Although I don't recommend using update
together with dispatch
, it's technically possible.
(As long as you don't use the Redux DevTools.)
We could implement useGlobalStateDispatch
, but that doesn't appeal much, does it?
const initialState = { counter: 0 }; // initialState is not optional.
const { GlobalStateProvider, useGlobalStateDispatch, useGlobalState } = createStore(reducer, initialState);
const Counter = () => {
const [value] = useGlobalState('counter');
const dispatch = useGlobalStateDispatch();
return (
<div>
<span>Counter: {value}</span>
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
</div>
);
};
from react-hooks-global-state.
Great explanation, man. Honestly, I never used Redux (more than for a few demo cases), but your explanations make sense to me. Thank you!
One more topic I'd like to discuss, if you don't mind:
Dispatch vs Update
Comparing dispatch and update-based approaches...
With the following:
const Counter = () => {
const [value, update] = useGlobalState('counter');
return (
<div>
<span>Counter: {value}</span>
<button onClick={() => update(v => v + 1)}>+1</button>
<button onClick={() => update(v => v - 1)}>-1</button>
</div>
);
};
we have pieces of logic incapsulated in JSX. They may be small enough to ignore or large enough to want to abstract away:
// actions.js
let actions = {
increment: (v) => v + 1,
decrement: (v) => v + 1,
}
// components/Counter.js
const Counter = () => {
const [value, update] = useGlobalState('counter');
return (
<div>
<span>Counter: {value}</span>
<button onClick={() => update(actions.increment)}>+1</button>
<button onClick={() => update(actions.decrement)}>-1</button>
</div>
);
};
- Do you use the above approach yourself?
It may be simplified, if the update
function is redesigned to return a callable:
// actions.js
let actions = {
increment: (v) => v + 1,
decrement: (v) => v + 1,
}
// components/Counter.js
const Counter = () => {
const [value, update] = useGlobalState('counter');
return (
<div>
<span>Counter: {value}</span>
<button onClick={update(actions.increment)}>+1</button>
<button onClick={update(actions.decrement)}>-1</button>
</div>
);
};
function update(fn) {
setState(fn)
}
// vs
function update(fn) {
return function () { setState(fn) }
}
or it can be exposed as a third function, I dunno:
const [value, update, update2] = useGlobalState('counter')
-
The benefit of both
useReducer
and the action-abstraction (mappers), shown above, is a separation between logic and templates which makes the logic easily testable. Of course, there are multiple approaches and personal prereferences, when it comes to testing. If ones favor E2E or high-level integration tests – maybe they don't this step. -
Now if we compare reducers and mappers, I believe the main benefit of
dispatch
is traceability. You can't make a comparable Redux Devtool analogy withupdate
(enclosed functions arguments are invisible, function names are unreliable when you use currying, etc). The main drawback is the extra complexity.
Would like to know your opinion on 1), 2) and 3).
from react-hooks-global-state.
Dispatch vs Update
In general, if you would like to follow "action-abstraction (mappers)", I'd suggest to use dispatch
.
So, I'd use dispatch
in this scenario, but still you could use update
. In this case, I would do like the following.
// actions.js
let actions = {
increment: (update) => () => update((v) => v + 1),
decrement: (update) => () => update((v) => v + 1),
}
// components/Counter.js
const Counter = () => {
const [value, update] = useGlobalState('counter');
return (
<div>
<span>Counter: {value}</span>
<button onClick={actions.increment(update)}>+1</button>
<button onClick={actions.decrement(update)}>-1</button>
</div>
);
};
You may not like this approach, though. Hm, maybe I wouldn't take this approach either.
Another option: <button onClick={applyAction(update, actions.increment)}>
Again, if you want to separate actions from components, just using dispatch
is natural.
However, we don't know about the best practice about the separation of logic and JSX in our new "hooks" world.
What I could imagine is something like this:
// components/CounterContainer.js
const CounterContainer = () => {
const [value, update] = useGlobalState('counter');
const increment = useCallback(() => update(v => v + 1), []);
const decrement = useCallback(() => update(v => v - 1), []);
return <ConterPresentation value={value} increment={increment} decrement={decrement} />;
};
// components/CounterPresentation.js
const CounterPresentation = ({ value, increment, decrement }) => (
<div>
<span>Counter: {value}</span>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
</div>
);
I actually like this one. It rather hides a global state in a container. It's more component-oriented approach. It seems to me to fit more with React philosophy.
Seems like it fits better with you to take the dispatch approach.
Like the previous example, the update
approach is to hide updating states in a component, so the mindset is probably the other way around.
function update(fn) {
return function () { setState(fn) }
}
This doesn't work if you need to take an argument like TextBox.
https://github.com/dai-shi/react-hooks-global-state/blob/master/examples/01_minimal/src/index.js#L28-L39
BTW, have you looked into the examples
folder? Feedback appreciated.
Please correct me if I misunderstood some of your questions.
from react-hooks-global-state.
I actually like this one. It rather hides a global state in a container. It's more component-oriented approach. It seems to me to fit more with React philosophy.
Hmm, I have to disagree here. In my opinion state changing logic should be as simple and straightforward as possible. A function or an object of functions are fine. Reducer is already slightly overcomplicated and I don't see other benefit than a) being more familiar for Redux users b) being more trace/debug friendly. Component is an overkill i.m.o. To test a component you need a fake DOM (an integration test) which is very slow in comparison to pure unit tests you could have otherwise.
BTW, have you looked into the examples folder? Feedback appreciated.
No but I need to. Thanks for the suggestion – I didn't notice them.
This doesn't work if you need to take an argument like TextBox.
Yeah, but I guess something like this can work:
function update(fn, ...args) {
return function () { setState(fn.bind(null, args)) }
}
I believe Hyperapp v1 had the API like this, may be wrong though.
from react-hooks-global-state.
Hmm, I have to disagree here.
Fair enough. Testing pure functions is always easier.
Yeah, but I guess something like this can work:
I see what you mean. I think we need arguments for the returning function.
What's good about hooks is that you can extend them by yourself based on primitives.
const { GlobalStateProvider, useGlobalState: useGlobalStateOrig } = createGlobalState(initialState);
function useGlobalState(name) {
const [state, setState] = useGlobalStateOrig(name);
function update(fn) {
return function(...args) { setState(fn(...args)); }
}
return [state, update];
}
from react-hooks-global-state.
What's good about hooks is that you can extend them by yourself based on primitives.
True story 👍 Need to experiment with that.
Do you plan to go in the direction of lensing:
useGlobalState('person');
// vs
useGlobalState('person.address');
// vs
useGlobalState(lensFrom(['person', 'address', ...]);
And yes, your examples are great! Gonna give this library a serious trial :)
from react-hooks-global-state.
This library is pretty much optimized for top-level selector (by observedBits
and type inference).
My recommendation for deep property value selection is something like that in examples/11_deep
.
Nevertheless, you could build deep selector based on primitives.
A naive implementation to get a deep value would be:
const useGlobalStateByPath = (path) => {
path = lodash.toPath(path);
const name = path.shift();
const [value] = useGlobalState(name);
return [lodash.get(value, path)];
};
(You need update
too for real lensing.)
from react-hooks-global-state.
You can accomplish global reducers/state without relying on the Context API or "global" dispatch functions:
https://github.com/jacob-ebey/react-hook-utils
from react-hooks-global-state.
Can you elaborate on what works better in concurrent mode?
from react-hooks-global-state.
The concurrent mode is still not fixed (it will change), and there could still be something I misunderstand. Anyway, here's a reference:
https://blog.isquaredsoftware.com/2018/11/react-redux-history-implementation/
In v6:
The Redux store state is put into an instance of the new createContext API
There is only one store subscriber: the component
This has all kinds of ripple effects across the implementation.
It's fair to ask why we chose to change this aspect. We certainly could have put the store instance into createContext, but there's several reasons why it made sense to put the store state into context instead.
The largest reason is to improve compatibility with "concurrent React", because the entire tree will see a single consistent state value. The very short explanation on this is that React's "time-slicing" and "Suspense" features can potentially cause problems when used with external synchronous state management tools. As one example, Andrew Clark has described "tearing" as a possible problem, where different parts of the component tree see different values during the same component tree re-render pass. By passing down the current state via context, we can ensure that the entire tree sees the same state value, because React takes care of that for us.
(This then turned out to be too early as there's not yet a way to bail out with useContext... ref: reduxjs/react-redux#1177)
On the other hand, this project is fine with context as long as we can use observedBits
. ref #5
from react-hooks-global-state.
Thanks for the link. Does "tearing" here refer to the visual representation of the vdom being out of sync current tree?
from react-hooks-global-state.
I too am not sure, but probably it's about different parts in the single vdom tree.
You can learn some more about it here. I think I follow the technique described there for my other library that uses subscriptions. I can't say for sure as the description is not very specific to hooks (at least for me).
from react-hooks-global-state.
Closing this issue. Feel free to open a new one.
from react-hooks-global-state.
Related Issues (20)
- getGlobalState should not be used in render HOT 5
- Error calling createStore (Typescript 3.6.4) HOT 2
- Question: how do developers use this library? HOT 1
- Please add CHANGELOG for 0.17.0 HOT 1
- getGlobalState loses sync with useGlobalState after 0.17 HOT 8
- How to use Redux Devtools Extension HOT 3
- comparison to react-tracked and initial values for a state HOT 9
- Error: You cannot use <GlobalStateProvider> more than once. HOT 5
- How to separate reducer and Action Type then call it to combineReducers (Ex: 07_middleware) HOT 3
- Offline saved data for React Native HOT 11
- Be able to access multiple state keys with a single hook HOT 3
- Mocking useGlobalState with jest? HOT 8
- Restoring initial state from localStorage, if any? HOT 10
- global state not preserved with Fast Refresh HOT 2
- Argument of type '"cart"' is not assignable to parameter of type 'never' HOT 5
- Asynchronous initial state HOT 4
- Is it possible to change a value outside a component? HOT 2
- Persistence with createGlobalState HOT 6
- How to easily reset state? HOT 1
- How to update the state in Bulk ? HOT 3
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from react-hooks-global-state.