Write local state using React Hooks and lift it up to React Context only when needed with minimum effort.
🕹 CodeSandbox demos 🕹 | ||||
---|---|---|---|---|
Counter | I18n | Theming | TypeScript | Wizard Form |
import React, { useState, useContext } from "react";
import createContainer from "constate";
// 1️⃣ Create a custom hook as usual
function useCounter() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return { count, increment };
}
// 2️⃣ Create container
const CounterContainer = createContainer(useCounter);
function Button() {
// 3️⃣ Use container context instead of custom hook
// const { increment } = useCounter();
const { increment } = useContext(CounterContainer.Context);
return <button onClick={increment}>+</button>;
}
function Count() {
// 4️⃣ Use container context in other components
// const { count } = useCounter();
const { count } = useContext(CounterContainer.Context);
return <span>{count}</span>;
}
function App() {
// 5️⃣ Wrap your components with container provider
return (
<CounterContainer.Provider>
<Count />
<Button />
</CounterContainer.Provider>
);
}
npm:
npm i constate
Yarn:
yarn add constate
Constate exports a single method called createContainer
. It receives two arguments: useValue
and createMemoInputs
(optional). And returns { Context, Provider }
.
It's a custom hook that returns the Context value:
import React, { useState } from "react";
import createContainer from "constate";
const CounterContainer = createContainer(() => {
const [count] = useState(0);
return count;
});
console.log(CounterContainer); // { Context, Provider }
You can receive arguments in the custom hook function. They will be populated with <Provider />
:
const CounterContainer = createContainer(({ initialCount = 0 }) => {
const [count] = useState(initialCount);
return count;
});
function App() {
return (
<CounterContainer.Provider initialCount={10}>
...
</CounterContainer.Provider>
);
}
The value returned in useValue
will be accessible when using useContext(CounterContainer.Context)
:
import React, { useContext } from "react";
function Counter() {
const count = useContext(CounterContainer.Context);
console.log(count); // 10
}
Optionally, you can pass in a function that receives the value
returned by useValue
and returns an array of inputs. When any input changes, value
gets re-evaluated, triggering a re-render on all consumers (components calling useContext()
).
If createMemoInputs
is undefined, it'll be re-evaluated everytime Provider
renders:
// re-render consumers only when value.count changes
const CounterContainer = createContainer(useCounter, value => [value.count]);
function useCounter() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return { count, increment };
}
This works similarly to the inputs
parameter in React.useEffect
and other React built-in hooks. In fact, Constate passes it to React.useMemo
inputs
internally.
You can also achieve the same behavior within the custom hook. This is an equivalent implementation:
import { useMemo } from "react";
const CounterContainer = createContainer(() => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
// same as passing `value => [value.count]` to `createMemoInputs` parameter
return useMemo(() => ({ count, increment }), [count]);
});
If you find a bug, please create an issue providing instructions to reproduce it. It's always very appreciable if you find the time to fix it. In this case, please submit a PR.
If you're a beginner, it'll be a pleasure to help you contribute. You can start by reading the beginner's guide to contributing to a GitHub project.
When working on this codebase, please use yarn
. Run yarn examples:start
to run examples.
MIT © Diego Haz