~ 1.5 kB React state management library that lets you write contextual state as if it were local state using React Hooks and React Context.
🕹 CodeSandbox demos 🕹 | ||
---|---|---|
Counter | Theming | I18n |
import React from "react";
import { Provider, useContextState } from "constate";
// 1. Create a custom hook
function useCounter(key) {
// 2. Replace React.useState(0) by useContextState(key, 0)
const [count, setCount] = useContextState(key, 0);
const increment = () => setCount(count + 1);
return { count, increment };
}
function Count() {
// 3. Consume the custom hook as usual
const { count } = useCounter("counter1");
return <span>{count}</span>
}
function IncrementButton() {
// 4. Consume the same key in other components
const { increment } = useCounter("counter1");
return <button onClick={increment}>+</button>;
}
function App() {
// 5. Wrap your app with Provider
return (
<Provider>
<Count />
<IncrementButton />
</Provider>
);
}
- Installation
Provider
useContextState
useContextReducer
useContextKey
useContextEffect
createContext
npm:
npm i constate@next
Yarn:
yarn add constate@next
You'll need to install
react@next
andreact-dom@next
Constate
v1
is currently in early alpha. If you're looking forv0
, seev0
docs or read the migration guide.
First, you should wrap your app (or the part using Constate) with Provider
so as to access contextual state within hooks:
import React from "react";
import { Provider } from "constate";
function App() {
return (
<Provider devtools={process.env.NODE_ENV === "development"}>
...
</Provider>
);
}
Passing devtools
prop to Provider
will enable the redux-devtools-extension integration, if that's installed in your browser. With that, you can easily debug the state of your application.
useContextState
has the same API as React.useState
, except that it receives contextKey
as the first argument. It can be either a string or the return value of useContextKey
.
All useContextState
calls with the same contextKey
throughout components in the Provider
tree will share the same state.
import { useContextState } from "constate";
function Component() {
// accesses state.contextKey in context
const [state, setState] = useContextState("contextKey", "initialValue");
...
}
If you pass null
or undefined
into the contextKey
parameter, it'll work exactly like React.useState
:
import { useContextState } from "constate";
function Component() {
// same as React.useState("initialValue")
const [state, setState] = useContextState(null, "initialValue");
...
}
This means you can create custom hooks that can be either contextual or local depending on the component using it:
import React from "react";
import { useContextState } from "constate";
function useCounter(key) {
const [count, setCount] = useContextState(key, 0);
const increment = () => setCount(count + 1);
return { count, increment };
}
function ContextualCounter() {
const { count, increment } = useCounter("counter1");
return <button onClick={increment}>{count}</button>;
}
function LocalCounter() {
const { count, increment } = useCounter();
return <button onClick={increment}>{count}</button>;
}
Just like useContextState
, useContextReducer
works similarly to React.useReducer
, but accepting a contextKey
argument, which can be either a string or the return value of useContextKey
:
import { useContextReducer } from "constate";
function reducer(state, action) {
switch(action.type) {
case "INCREMENT": return state + 1;
case "DECREMENT": return state - 1;
default: return state;
}
}
function useCounter(key) {
const [count, dispatch] = useContextReducer(key, reducer, 0);
const increment = () => dispatch({ type: "INCREMENT" });
const decrement = () => dispatch({ type: "DECREMENT" });
return { count, increment, decrement };
}
function ContextualCounter() {
const { count, increment } = useCounter("counter1");
return <button onClick={increment}>{count}</button>;
}
Instead of passing strings to useContextState
and useContextReducer
, you can create a reference to the context key.
import { useContextKey } from "constate";
function Counter() {
const key = useContextKey("counter1");
const [count, setCount] = useContextState(key, 0);
...
}
It uses React.useRef
underneath and is required when using useContextEffect
.
Constate provides all contextual versions of React.useEffect
, such as useContextEffect
and useContextLayoutEffect
.
They receive contextKey
as the first argument. Unlike useContextState
and useContextReducer
, it's limited to the value returned by useContextKey
. If contextKey
is null
or undefined
, the hook will work exactly as the React one.
import { Provider, useContextKey, useContextEffect } from "constate";
let count = 0;
function useCounter(context) {
// useContextKey is required for effects
const key = useContextKey(context);
useContextEffect(key, () => {
count += 1;
}, []);
}
function ContextualCounter1() {
useCounter("counter1");
...
}
function ContextualCounter2() {
useCounter("counter1");
...
}
function App() {
return (
<Provider>
<ContextualCounter1 />
<ContextualCounter2 />
</Provider>
);
}
In the example above, if we were using React.useEffect
, count
would be 2
. With useContextEffect
, it's 1
.
useContextEffect
ensures that the function will be called only once per contextKey
no matter how many components are using it.
If you want to set a initial state for the whole context tree and/or want to create separate contexts, you can use createContext
:
// MyContext.js
import { createContext } from "constate";
const {
Provider,
useContextKey,
useContextState,
useContextReducer,
useContextEffect,
useContextLayoutEffect
} = createContext({
counter1: 0,
posts: [
{ id: 1, title: "Hello World!" }
]
});
export {
Provider,
useContextKey,
useContextState,
useContextReducer,
useContextEffect,
useContextLayoutEffect
};
// App.js
import React from "react";
import { Provider, useContextState } from "./MyContext";
function Counter() {
// no need for initial value, it has been set in context
const [count, setCount] = useContextState("counter1");
const increment = () => setCount(count + 1);
return <button onClick={increment}>{count}</button>;
}
function App() {
return (
<Provider>
<Counter />
</Provider>
);
}
When importing hooks directly from the
constate
package, you're, in fact, using a default context created by our index file.
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