react-super-context
A tiny wrapper library around the React Context API that removes a lot of boilerplate required to create and consume contexts.
Features
- Create contexts with no boilerplate
- No more nested context providers
- Throws an error when consuming a context that has no provider instead of failing silently
- Built with TypeScript
- Small bundle size
Installation
npm i react-super-context
Before and After
Before
interface CounterContextModel {
count: number;
increment: () => void;
decrement: () => void;
}
// createContext expects a default value that is used if there are no providers for the context
const CounterContext = createContext<CounterContextModel>({
count: 0,
increment: () => {},
decrement: () => {},
});
// we export a provider component that is responsible for the context's states
export const CounterContextProvider = ({ children }: PropsWithChildren<{}>) => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
const decrement = () => setCount(Math.max(0, count - 1));
return (
<CounterContext.Provider value={{ count, increment, decrement }}>
{children}
</CounterContext.Provider>
);
};
// we also export a hook that can be used to consume the context in our components
export const useCounter = () => useContext(CounterContext);
After
const [CounterContext, useCounter] = createSuperContext(() => {
// the state logic is the same as before
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
const decrement = () => setCount(Math.max(0, count - 1));
// we now simply have to return the context's value
// when using TypeScript, the types are inferred and the useCounter hook will have proper type hints
return { count, increment, decrement };
});
export { CounterContext, useCounter };
Before
return (
<CounterContextProvider>
<MySecondContextProvider>
<MyThirdContextProvider>
<div>Your app with consumers comes here</div>
</MyThirdContextProvider>
</MySecondContextProvider>
</CounterContextProvider>
);
};
After
<SuperContext contexts={[CounterContext, MySecondContext, MyThirdContext]}>
<div>Your app with consumers comes here</div>
</SuperContext>
);
Examples
1. Simple example
1. Use the createSuperContext function to create your context. It takes a factory function that returns the context's value and returns a context object as well as a hook to consume the state.
const [CounterContext, useCounter] = createSuperContext(() => {
const [count, setCount] = useState(0);
return {count, setCount};
});
export { CounterContext, useCounter };
2. To create a provider for the context, add the SuperContext component in your app and pass it the CounterContext created by the createSuperContext call.
const App = () => (
<SuperContext contexts={[CounterContext]}>
<CountDisplay/>
<CounterButton/>
</SuperContext>
);
3. Consume the context in your components using the useCounter hook.
const CountDisplay = () => {
const { count } = useCounter();
return <div>{count}</div>;
};
// CounterButton.tsx
const CounterButton = () => {
const { count, setCount } = useCounter();
return <button onClick={() => setCount(count + 1)}>+1</button>;
};
2. Use multiple contexts
1. Create a second context that uses useCounter.
const [EvenOrOddContext, useEvenOrOdd] = createSuperContext(() => {
const { count } = useCounter();
return count % 2 === 0 ? "even" : "odd";
});
export { EvenOrOddContext, useEvenOrOdd };
2. Remember to add it to the contexts lists. The order of the contexts matters.
const App = () => (
<SuperContext contexts={[CounterContext, EvenOrOddContext]}>
<CountDisplay/>
<CounterButton/>
</SuperContext>
);
EvenOrOddContext depends on CounterContext so if they were given the other way around (contexts={[EvenOrOddContext, CounterContext]}), then the useCounter call in EvenOrOddContext.ts will throw an error.
3. Consume the new context.
export const CountDisplay = () => {
const { count } = useCounter();
const evenOrOdd = useEvenOrOdd();
return <div>{count} ({evenOrOdd})</div>;
};
3. Use hooks as you normally would
const { count } = useCounter();
const evenOrOdd = useEvenOrOdd();
useEffect(() => {
console.log(`The current count is ${count} which is ${evenOrOdd}`);
}, [count, evenOrOdd]);
});
export default Logging;
Remember to always add your context objects to the SuperContext component.
4. Passing props
1. Create a super context with the desired props.
interface CounterContextProps {
initial: number;
}
const [CounterContext, useCounter] = createSuperContext(({ initial }: CounterContextProps) => {
const [count, setCount] = useState(initial);
return { count, setCount };
});
export { CounterContext, useCounter };
2. CounterContext is a function that you can pass the props to.
const App = () => (
<SuperContext contexts={[CounterContext({ initial: 10 })]}>
<CountDisplay/>
<CounterButton/>
SuperContext>
);
5. TypeScript
In all the examples above, TypeScript is able to infer the types of both the context's value (the value returned by the factory function and by the generated hook) and the contexts' props.
const { count } = useCounter(); // inferred type: { count: number, increment: () => void, decrement: () => void }
const evenOrOdd = useEvenOrOdd(); // inferred type: "even" | "odd"
return <div>{count} ({evenOrOdd})</div>;
};
However, you can also define types explicitly:
1. Type given explicitly in createSuperContext call.
interface CounterContextModel {
count: number;
increment: () => void;
decrement: () => void;
}
const [CounterContext, useCounter] = createSuperContext<CounterContextModel>(() => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
const decrement = () => setCount(Math.max(0, count - 1));
return { count, increment, decrement };
});
2. Type inferred when consuming the context.
const { count } = useCounter(); // inferred type: CounterContext
return <div>{count}</div>;
};
6. Using props with Typescript
The simplest approach is to define the prop type in the argument for the factory function.
initial: number;
}
const [CounterContext, useCounter] = createSuperContext(({ initial }: CounterContextProps) => {
const [count, setCount] = useState(initial);
return { count, setCount };
});
If you have defined the context's value type explicitly, you must pass the prop type as the second generic argument (at least until TypeScript gets support for partial type argument inference).
const [count, setCount] = useState(initial);
return { count, setCount };
});
7. Options
The createSuperContext function takes an optional object as the second argument, allowing you to specify a number of options.
() => {
const [count, setCount] = useState(0);
return { count, setCount };
},
{
displayName: "MyCounterContext",
testValue: { count: 0, setCount: () => {} },
}
);
displayName will be the name of the context provider component in error messages. The testValue is the value returned by the useCounter hook in a test environment. The library will by default check if NODE_ENV === "test" to determine if it is in a test environment, but this can be overridden with the testEnvironment option.
If you use many of the same options on all context provided by a SuperContext, you can use the defaultOptions prop to set defaults:
<SuperContext
contexts={[CounterContext, EvenOrOddContext]}
defaultOptions={{displayName: "MyContext"}}
>...</SuperContext>
);
In the example above, both the CounterContext and the EvenOrOddContext provider components will be displayed as "MyContext" in error messages.