Skip to content

Instantly share code, notes, and snippets.

@mikaelbr
Last active July 15, 2022 19:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mikaelbr/45ef683e894b0b4da6d91b599dda1289 to your computer and use it in GitHub Desktop.
Save mikaelbr/45ef683e894b0b4da6d91b599dda1289 to your computer and use it in GitHub Desktop.
React Context Module Pattern - Gists for blogpost
// in App.tsx
type AlertWithId = {
message: string;
type: "info" | "error";
id: number;
};
// Part 1: The container to hold values
const AlertContext = createContext<AlertWithId[]>([]);
function Content() {
// Part 2: Here we access the data from behind the scenes.
const alerts = useContext(AlertContext);
return (
<div>
{alerts.map((i) => (
<div key={i.id}>
{i.type}: {i.message}
</div>
))}
</div>
);
}
function App() {
// Part 3: The state
const [alerts, setAlerts] = useState<AlertWithId[]>([]);
// Part 1: Populate value to the provider
return (
<AlertContext.Provider value={alerts}>
<Content />
</AlertContext.Provider>
);
}
const initialState: AlertContextState = {
alerts: [],
+ // Add a no-op that satisifes the type definition
+ addAlert: (i) => ({ ...i, id: 0 }),
};
// === in alert-context.ts ===
export type Alert = {
message: string;
type: "info" | "error";
};
export type AlertWithId = Alert & { id: number };
type AlertContextState = {
alerts: AlertWithId[];
addAlert(alert: Alert): AlertWithId;
};
const initialState: AlertContextState = {
alerts: [],
addAlert: (i) => ({ ...i, id: 0 }),
};
// Note we export the AlertContext
export const AlertContext = createContext<AlertContextState>(initialState);
// === in our App.tsx ===
import { AlertContext } from "./alert-context";
function App() {
const [alerts, setAlerts] = useState<AlertWithId[]>([]);
const addAlert = useCallback((alert: Alert) => {
const alertWithId = {
...alert,
id: Date.now(),
};
setAlerts((all) => [alertWithId].concat(all));
return alertWithId;
}, []);
return (
<AlertContext.Provider value={{ alerts, addAlert }}>
<MyComponent />
</AlertContext.Provider>
);
// ...
}
// === and in component ===
import { AlertContext } from "./alert-context";
function MyComponent({ message }: { message: string }) {
const { alerts, addAlert } = useContext(AlertContext);
// ...
}
// in alert-context.ts
export function useAlertContextState() {
const [alerts, setAlerts] = useState<AlertWithId[]>([]);
const addAlert = useCallback((alert: Alert) => {
const alertWithId = {
...alert,
id: Date.now(),
};
setAlerts((all) => [alertWithId].concat(all));
return alertWithId;
}, []);
return { alerts, addAlert };
}
// in app.ts
import { AlertContext, useAlertContextState } from "./alert-context";
function App() {
const state = useAlertContextState();
return (
<AlertContext.Provider value={state}>
<MyComponent />
</AlertContext.Provider>
);
}
// in alert-context.tsx
// NOTE: Now we have to rename it to .tsx as it has JSX.
// Note we export the AlertContext
export const AlertContext = createContext<AlertContextState>(initialState);
type AlertContextProviderProps = PropsWithChildren<{}>;
export function AlertContextProvider({ children }: AlertContextProviderProps) {
const [alerts, setAlerts] = useState<AlertWithId[]>([]);
const addAlert = useCallback((alert: Alert) => {
const alertWithId = {
...alert,
id: Date.now(),
};
setAlerts((all) => [alertWithId].concat(all));
return alertWithId;
}, []);
return (
<AlertContext.Provider value={state}>{children}</AlertContext.Provider>
);
}
-import { AlertContext, useAlertContextState } from "./alert-context";
+import { AlertContextProvider } from "./alert-context";
function App() {
- const state = useAlertContextState();
-
return (
- <AlertContext.Provider value={state}>
+ <AlertContextProvider>
<MyComponent />
- </AlertContext.Provider>
+ </AlertContextProvider>
);
}
// in alert-context.tsx
type AlertContextProviderProps = PropsWithChildren<{
+ initialAlerts: AlertWithId[];
}>;
export function AlertContextProvider({
children,
+ initialAlerts,
}: AlertContextProviderProps) {
- const [alerts, setAlerts] = useState<AlertWithId[]>([]);
+ const [alerts, setAlerts] = useState<AlertWithId[]>(initialAlerts);
// ...
}
// in App.tsx
import { AlertContextProvider } from "./alert-context";
function App() {
return (
- <AlertContextProvider>
+ <AlertContextProvider initialAlerts={[myAlert]}>
<MyComponent />
</AlertContextProvider>
);
}
// Importing the AlertContext manully
import { useContext } from "react";
import { AlertContext } from "./alert-context";
function MyComponent({ message }: { message: string }) {
// Passing context to useContext
const { alerts, addAlert } = useContext(AlertContext);
// ...
}
// in alert-context.tsx
// Now we can also hide AlertContext from the rest of the world by not exporting it
-export const AlertContext = createContext<AlertContextState>(initialState);
+const AlertContext = createContext<AlertContextState>(initialState);
// Export our own custom hook
+export function useAlert() {
+ return useContext(AlertContext);
+}
// Importing the AlertContext manully
-import { useContext } from "react";
-import { AlertContext } from "./alert-context";
+import { useAlert } from "./alert-context";
function MyComponent({ message }: { message: string }) {
// Passing context to useContext
- const { alerts, addAlert } = useContext(AlertContext);
+ const { alerts, addAlert } = useAlert();
// ...
}
// file: DESCRIPTIVE_NAME-context.tsx
// 1. Domain modes
type DESCRIPTIVE_NAME = {};
// 2. Context types
type DESCRIPTIVE_NAMEContextState = {};
const initialState: DESCRIPTIVE_NAMEContextState = {};
// 3. Creating context
const DESCRIPTIVE_NAMEContext =
createContext<DESCRIPTIVE_NAMEContextState>(initialState);
// 4. Hiding context by providing wanted API.
export type DESCRIPTIVE_NAMEContextProviderProps = PropsWithChildren<{}>;
export function DESCRIPTIVE_NAMEContextProvider({
children,
}: DESCRIPTIVE_NAMEContextProviderProps) {
return (
<DESCRIPTIVE_NAMEContext.Provider value={{}}>
{children}
</DESCRIPTIVE_NAMEContext.Provider>
);
}
export function useDESCRIPTIVE_NAME() {}
type AlertContextState = {
alerts: AlertWithId[];
};
const initialState: AlertContextState = {
alerts: [],
};
const AlertContext = createContext<AlertContextState>(initialState);
function App() {
const [alerts, setAlerts] = useState<AlertWithId[]>([]);
return (
<AlertContext.Provider value={{ alerts }}>
<Content />
</AlertContext.Provider>
);
}
// Using it
function Content() {
const { alerts } = useContext(AlertContext);
}
function App() {
return (
<AlertContextProvider initialAlerts={[myAlert]}>
<MyComponent />
</AlertContextProvider>
);
}
// in component
function MyComponent({ message }: { message: string }) {
const { alerts, addAlert } = useAlert();
// ...
}
function App() {
return (
- <AlertContextProvider initialAlerts={[myAlert]}>
<MyComponent />
- </AlertContextProvider>
);
}
+const initialState: AlertContextState = {
+ alerts: [],
+ addAlert: (i) => ({ ...i, id: 0 }),
+};
// Passing in default state
-const AlertContext = createContext<AlertContextState>();
+const AlertContext = createContext<AlertContextState>(initialState);
// in alert-context.tsx
// Note: No longer initial state, and extended to be either state or undefined
-const AlertContext = createContext<AlertContextState>(initialState);
+const AlertContext = createContext<AlertContextState | undefined>();
// ...
// We've wrapped this, making it easier to change.
export function useAlert(): AlertsState {
const context = useContext(AlertContext);
+ if (context === undefined) {
+ throw new Error("useAlert must be used within a AlertContextProvider");
+ }
return context;
}
// Note: Introduce Alert along with AlertWithId
// to support adding alert without id as input.
type Alert = {
message: string;
type: "info" | "error";
};
type AlertWithId = Alert & { id: number };
function App() {
const [alerts, setAlerts] = useState<AlertWithId[]>([]);
const addAlert = (alert: Alert) => {
const alertWithId = {
...alert,
id: Date.now(),
};
// Insert at top
setAlerts((all) => [alertWithId].concat(all));
return alertWithId;
};
// Note: Adding function.
return (
<AlertContext.Provider value={{ alerts, addAlert }}>
<Content />
</AlertContext.Provider>
);
}
function Content() {
// Still works as before
const { alerts } = useContext(AlertContext);
}
function MyComponent() {
// Still works as before
const { alerts, addAlert } = useContext(AlertContext);
const add = () => {
addAlert({ type: "info", message: "Hello" });
};
return (
<div>
<button onClick={add}>Add new</button>
</div>
);
}
function MyComponent({message}: {message: string}) {
// Still works as before
const { alerts, addAlert } = useContext(AlertContext);
useEffect({
addAlert({ type: "info", message: "Hello" });
}, [message, addAlert]);
// ...
}
function App() {
// ...
- const addAlert = (alert: Alert) => {
+ const addAlert = useCallback((alert: Alert) => {
const alertWithId = {
...alert,
id: Date.now(),
};
// Insert at top
setAlerts((all) => [alertWithId].concat(all));
return alertWithId;
- };
+ }, []);
// ...
}
<AlertContext.Provider value={{ alerts, addAlert }}>
// ----------------------------------------^
// ⤷ 'addAlert' does not exist in type 'AlertsState'
type AlertContextState = {
alerts: AlertWithId[];
+ addAlert(alert: Alert): AlertWithId;
};
// Property 'addAlert' is missing in type '{ alerts: never[]; }'
// ⥅ but required in type 'AlertsState'.
const initialState: AlertContextState = {
alerts: [],
};
import {
createContext,
PropsWithChildren,
useCallback,
useContext,
useState,
} from "react";
import useInterval from "./use-interval";
// 0. Constants
const UPDATE_FREQUENCY_MS = 1000;
// 1. Relevant domain types
type Alert = {
message: string;
type: "info" | "error";
};
type AlertWithId = { id: number } & Alert;
// 2. Context types
type AlertsState = {
alerts: AlertWithId[];
addAlert(alert: Alert): AlertWithId;
removeAlert(alert: AlertWithId): AlertWithId;
cleanAlerts(): void;
};
// 3. Creating context
const AlertContext = createContext<AlertsState | undefined>(undefined);
// 4. Hiding context by providing wanted API.
export type AlertContextProps = PropsWithChildren<{
initialAlerts?: AlertWithId[];
timeoutInMs?: number;
}>;
export function AlertContextProvider({
children,
initialAlerts = [],
timeoutInMs = 5000,
}: AlertContextProps) {
const [alerts, setAlerts] = useState(initialAlerts);
const addAlert = useCallback((alert: Alert) => {
const alertWithId = {
...alert,
id: Date.now(),
};
// Insert at top
setAlerts((all) => [alertWithId].concat(all));
return alertWithId;
}, []);
const removeAlert = useCallback((alert: AlertWithId) => {
// Filter out alert
setAlerts((all) => all.filter((i) => i.id !== alert.id));
return alert;
}, []);
const filterOldAlerts = useCallback(() => {
// Filter out alert
if (!alerts.length) return;
const threshold = Date.now() - timeoutInMs;
setAlerts((all) => all.filter((i) => i.id > threshold));
}, [alerts.length, timeoutInMs]);
const cleanAlerts = useCallback(() => {
setAlerts([]);
}, []);
useInterval(filterOldAlerts, UPDATE_FREQUENCY_MS);
return (
<AlertContext.Provider
value={{
alerts,
addAlert,
removeAlert,
cleanAlerts,
}}
>
{children}
</AlertContext.Provider>
);
}
// 5. Exporting context accessor
export function useAlerts(): AlertsState {
const context = useContext(AlertContext);
if (context === undefined) {
throw new Error("useAlerts must be used within a AlertContextProvider");
}
return context;
}
{
"React Context Module": {
"prefix": "reactcontext",
"body": [
"import { useContext, createContext, PropsWithChildren } from \"react\";",
"",
"type $1 = {$2};",
"",
"type $1ContextState = {$3};",
"",
"",
"const $1Context =",
" createContext<$1ContextState | undefined>(undefined);",
"",
"",
"export type $1ContextProviderProps = PropsWithChildren<{}>;",
"export function $1ContextProvider({",
" children,",
"}: $1ContextProviderProps) {",
" $0",
"",
" return (",
" <$1Context.Provider value={{}}>",
" {children}",
" </$1Context.Provider>",
" );",
"}",
"",
"export function use$1() {",
" const context = useContext($1Context);",
" if (context === undefined) {",
" throw new Error('use$1 must be used within $1ContextProvider');",
" }",
" return context;",
"}",
""
],
"description": "React Context Module Pattern"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment