Skip to content

Instantly share code, notes, and snippets.

@JLarky
Last active October 29, 2024 14:16
Show Gist options
  • Save JLarky/5a1642abd8741f2683a817f36dd48e78 to your computer and use it in GitHub Desktop.
Save JLarky/5a1642abd8741f2683a817f36dd48e78 to your computer and use it in GitHub Desktop.
Ultimate example of react context hook with nice type-safe (TypeScript) wrappers and reduced boilerplate by using `ReturnType`
import React from "react";
import { useImmer } from "use-immer";
function useProviderValue() {
const [moved, setMoved] = React.useState(false);
const [point, setPoint] = useImmer<{
x: number;
y: number;
}>({ x: 0, y: 0 }); // using immer to illustrate that you can easily derive setPoint type instead of writing types for Context manually
const value = React.useMemo(
() => ({
moved,
setMoved,
point,
setPoint,
}),
[moved, point, setPoint]
);
return value;
}
export type Context = ReturnType<typeof useProviderValue>;
const Context = React.createContext<Context | undefined>(undefined);
Context.displayName = "Context";
export const Provider = (props: React.PropsWithChildren) => {
const value = useProviderValue();
return <Context.Provider value={value} {...props} />;
};
export function useContext() {
const context = React.useContext(Context);
if (context === undefined) {
throw new Error(`useContext must be used within a Provider`);
}
return context;
}
export function useMoved() {
const { moved } = useContext();
return moved;
}
export function useListenMouseMove() {
const { setMoved, setPoint } = useContext();
const isMounted = React.useRef(false);
React.useEffect(() => {
isMounted.current = true;
const listen = (e: MouseEvent) => {
if (isMounted.current) {
setPoint(draft => {
draft.x = e.x;
draft.y = e.y;
});
setMoved(true);
}
};
document.addEventListener("mousemove", listen);
return () => {
isMounted.current = false;
document.removeEventListener("mousemove", listen);
};
}, [setMoved, setPoint]);
return;
}
export const Example = () => {
return (
<Provider>
<Test1 />
<Test2 />
</Provider>
);
};
const Test1 = () => {
useListenMouseMove();
return null;
};
const Test2 = () => {
const { point } = useContext();
const hasMoved = useMoved();
return (
<>
{hasMoved && (
<span>
({point.x},{point.y})
</span>
)}
</>
);
};
// minimal example
import type { PropsWithChildren } from "react";
import { createContext, useContext, useMemo, useState } from "react";
function useProviderValue() {
const [isDark, setIsDark] = useState(false);
return useMemo(() => ({ isDark, setIsDark }), []);
}
export type Context = ReturnType<typeof useProviderValue>;
const DarkContext = createContext<Context | undefined>(undefined);
DarkContext.displayName = "DarkProvider";
export const DarkProvider = (props: PropsWithChildren) => {
const value = useProviderValue();
return <DarkContext.Provider value={value} {...props} />;
};
export function useDark() {
const context = useContext(DarkContext);
if (context === undefined) {
throw new Error(`useDark must be used within a DarkProvider`);
}
return context;
}
export function useIsDark() {
return useDark().isDark;
}
export function useSetDark() {
return useDark().setIsDark;
}
import type { PropsWithChildren } from "react";
import { createContext, useContext, useMemo, useState } from "react";
function useProviderValue() {
const [isDark, setIsDark] = useState(false);
const value = useMemo(
() => ({
isDark,
setIsDark,
}),
[]
);
return value;
}
export type Context = ReturnType<typeof useProviderValue>;
const DarkContext = createContext<Context>({
isDark: false,
setIsDark: () => {},
});
DarkContext.displayName = "DarkProvider";
export const DarkProvider = (props: PropsWithChildren) => {
const value = useProviderValue();
return <DarkContext.Provider value={value} {...props} />;
};
export function useDark() {
return useContext(DarkContext);
}
export function useIsDark() {
return useDark().isDark;
}
export function useSetDark() {
return useDark().setIsDark;
}
@abhisekpadhi
Copy link

Neat 👍

@kutsan
Copy link

kutsan commented Feb 13, 2021

Thanks! ReturnType to the rescue!

@TafkaMax
Copy link

TafkaMax commented Mar 3, 2021

what if i dont have a return type for the useprovidervalue? I just have useState set in there and nothing else.

EDIT: Should i return the useState variable. so const[variable, setVariable] -> in the end i return the variable, so my the context gets the return type of the state.

@JLarky
Copy link
Author

JLarky commented Mar 4, 2021

what if i dont have a return type for the useprovidervalue? I just have useState set in there and nothing else.

@TafkaMax, I would do something like this:

function useProviderValue() {
  const [moved, setMoved] = React.useState(false);
  return [moved, setMoved] as const;
}

@tilucast
Copy link

Thank you dude. I've spent two days trying to figure out how to type context efficiently.

@binlf
Copy link

binlf commented Dec 10, 2023

nice

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment