tiny react context/state manager with hooks
npm install --save noctx
- concise api:
getCtx()
,setCtx()
,useCtx()
,initValue
, check full examples. - think in hooks
import React, { useState } from "react";
import { render } from "react-dom";
import noctx from "noctx";
const { setCtx, getCtx } = noctx()
function useCounter(initState = 0) {
const [counter, setCounter] = useState(initState);
const increment = () => setCounter(e => e + 1);
return { counter, increment };
}
const Counter = setCtx("counter", useCounter);
const CounterDisplay = () => {
const { counter, increment } = getCtx("counter");
return (
<div>
<p>Counter: {counter}</p>
<button onClick={increment}>Counter Increment</button>
</div>
);
};
function App() {
return (
<Counter.Provider>
<CounterDisplay />
</Counter.Provider>
);
}
render(<App />, document.getElementById("root"));
./examples/index.js
import React, { useState, useReducer } from 'react';
import { render } from 'react-dom';
import noctx from 'noctx';
const { setCtx, getCtx, useCtx } = noctx()
function useCounter(initValue = 0) {
const [counter, setCounter] = useState(initValue);
const increment = () => setCounter(e => e + 1);
return { counter, increment };
}
// you can use immer.js if preferred
// you can add payload if any
const thirdCounterReducer = (state, action) => {
switch (action.type) {
case 'decrement':
return { ...state, number: state.number - 1 }
case 'increment':
return { ...state, number: state.number + 1 }
default:
return state
}
}
function useThirdCounter(initValue) {
const initState = initValue || { number: 0 }
const [state, dispatch] = useReducer(thirdCounterReducer, initState);
return [state, dispatch]
}
const Counter = setCtx('counter', useCounter);
const AnotherCounter = setCtx('anotherCounter', useCounter);
const ThirdCounter = setCtx('thirdCounter', useThirdCounter);
const CounterDisplay = () => {
const { counter, increment } = getCtx('counter');
console.log('CounterDisplay')
return (
<div>
<p>Counter: {counter}</p>
<button onClick={increment}>Counter Increment</button>
</div>
);
};
const AnotherCounterDisplay = () => {
const { counter, increment } = useCtx(AnotherCounter);
// if uncomment this line, it will be rerendered if counter in CounterDisplay changes
// const { counter: counter2, increment: increment2 } = useCtx(Counter);
console.log('AnotherCounterDisplay')
return (
<div>
<p>Another Counter: {counter}</p>
<button onClick={increment}>Another Counter Increment</button>
</div>
);
};
const ThirdCounterDisplay = () => {
const [[,dispatch]] = getCtx(['thirdCounter']);
console.log('ThirdCounterDisplay')
return (
<div>
<button onClick={() => dispatch({ type: 'increment' })}>3rd Counter Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>3rd Counter Decrement</button>
</div>
);
};
const AllCounterDisplay = () => {
const [
{ counter },
{ counter: secCounter },
[ThirdCounterState]
] = getCtx(['counter', 'anotherCounter', 'thirdCounter']);
console.log('AllCounterDisplay')
return (
<div>
<p>1st Counter: {counter}</p>
<p>2nd Counter: {secCounter}</p>
<p>3rd Counter: {ThirdCounterState.number}</p>
</div>
);
};
function App() {
return (
<Counter.Provider>
<AnotherCounter.Provider initValue={2}>
<ThirdCounter.Provider>
<div>
<CounterDisplay />
<AnotherCounterDisplay />
<AllCounterDisplay />
<ThirdCounterDisplay />
</div>
</ThirdCounter.Provider>
</AnotherCounter.Provider>
</Counter.Provider>
);
}
render(<App />, document.getElementById('root'));
-
The component with
getCtx('counter')
is always updated within the component state changes regardless the context value is changed or not. The reason is here and example is sandbox.- if you add some local state changes (e.g. add a setState
[toggle, setToggle]
insideCounterDisplay
, it will be fully rerendered when you setToggle, unless you isolate the logic and use useMemo and memo at the same time, refer to the sandbox above - in the full example above, if the button "Counter Increment" is clicked, the
CounterDisplay
andThirdCounterDisplay
will be updated since they are referring to context 'counter' - As of now based on the discussion and practice, the best way to avoid rerendering is to isolate components only with their minimum necessary state / context, so they will not affect each other if non-shared state is changed. Here is the support article: mobx: react-performance
- if you add
getCtx('counter')
oruseCtx(Counter)
inAnotherCounterDisplay
but do not use it, after click the button "Counter Increment", all 3 components are still rerendered. - in additional, refer to this answer
- to track the provider value, refer to this
- if you add some local state changes (e.g. add a setState
-
If you prefer to combine the dependency states, you need to refer to the the sandbox above, use useMemo and memo at the same time
-
There might be some potential solution based on this
-
Known downside: there is no server-side rendering support as of now.
-
proposal of useReducer: result is: useReducer will not prevent rerendering, see codes in full example; The reason in this sandbox dispatch does not trigger rerender is: it actaully spilts State.Provider and Dispatch.Provider. This trick may be helpful for those are intersted, but will not be accepted until there is a more efficient way.
- https://github.com/slorber/react-async-hook
- https://github.com/slorber/awesome-debounce-promise
- https://github.com/Andarist/use-constant
- https://nikgraf.github.io/react-hooks/
- use typescript
- add test case
HIGHLY inspired by unstated-next