Skip to content

Instantly share code, notes, and snippets.

@pjchender
Last active September 13, 2022 06:55
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 pjchender/afb3149f7f0b5ef4149adc84b4ff51f4 to your computer and use it in GitHub Desktop.
Save pjchender/afb3149f7f0b5ef4149adc84b4ff51f4 to your computer and use it in GitHub Desktop.
useContext + useReducer with TypeScript
import * as React from 'react';
import { ChangeEvent } from 'react';
import { ColorSlider } from './ColorSlider';
import { useContext } from './rgb-context';
export interface AdjustmentInputProps
extends React.HTMLProps<HTMLInputElement> {
id: string;
label: string;
value: number;
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
}
export interface ColorAdjustmentProps {
Adjustment: React.ComponentType<AdjustmentInputProps>;
}
export const ColorAdjustment = ({ Adjustment }: ColorAdjustmentProps) => {
// STEP 6:從 createContext utility 中取出該 context 內的資料
const { red, green, blue, dispatch } = useContext();
const adjustRed = (event: ChangeEvent<HTMLInputElement>) => {
dispatch({ type: 'ADJUST_RED', payload: +event.target.value });
};
const adjustGreen = (event: ChangeEvent<HTMLInputElement>) => {
dispatch({ type: 'ADJUST_GREEN', payload: +event.target.value });
};
const adjustBlue = (event: ChangeEvent<HTMLInputElement>) => {
dispatch({ type: 'ADJUST_BLUE', payload: +event.target.value });
};
return (
<section className="color-sliders">
<Adjustment
id="red-slider"
label="Red"
value={red}
onChange={adjustRed}
/>
<Adjustment
id="green-slider"
label="Green"
value={green}
onChange={adjustGreen}
/>
<Adjustment
id="blue-slider"
label="Blue"
value={blue}
onChange={adjustBlue}
/>
</section>
);
};
// create-context.tsx
// https://frontendmasters.com/courses/react-typescript/context-api-edge-cases/
// STEP 1:建立 createContext 這個 utility
// <A extends {} | null>:A 需要滿足是任何的物件,否則是 null
export function createContext<A extends {} | null>() {
const ctx = React.createContext<A | undefined>(undefined);
const useContext = () => {
const c = React.useContext(ctx);
if (c === undefined) {
throw new Error('useContext must be inside a Provider with a value');
}
return c;
};
// as const:讓這個 tuple 是 read only
return [useContext, ctx.Provider] as const;
}
import './style.scss';
import { render } from 'react-dom';
import Application from './Application';
import { RGBContextProvider } from './rgb-context';
import { ThemeProvider } from './theme-context';
const rootElement = document.getElementById('root');
// STEP 5:使用 RGBContextProvider
render(
<ThemeProvider>
<RGBContextProvider>
<Application />
</RGBContextProvider>
</ThemeProvider>,
rootElement
);
// https://frontendmasters.com/courses/react-typescript/context-api-edge-cases/
// STEP 3:撰寫 reducer
import { RGBColorType } from './types';
const colors = ['red', 'green', 'blue'] as const;
type Colors = Uppercase<typeof colors[number]>;
type ActionTypes = `ADJUST_${Colors}`; // "ADJUST_RED" | "ADJUST_GREEN" | "ADJUST_BLUE"
export type AdjustmentAction = {
type: ActionTypes;
payload: number;
};
export const reducer = (
state: RGBColorType,
action: AdjustmentAction
): RGBColorType => {
for (const color of colors) {
if (action.type === `ADJUST_${color.toUpperCase()}`) {
return { ...state, [color]: action.payload };
}
}
return state;
};
// contexts/RGBContext.tsx
// https://frontendmasters.com/courses/react-typescript/context-api-edge-cases/
import * as React from 'react';
import { createContext } from './create-context';
import { AdjustmentAction, reducer } from './reducer';
import { RGBColorType } from './types';
interface RGBContextType extends RGBColorType {
dispatch: React.Dispatch<AdjustmentAction>;
}
// STEP 2 使用 createContext uility 來建立 context 和 Provider
// 透過我們客製化的 createContext 來讓 TS 知道 context 的型別
export const [useContext, Provider] = createContext<RGBContextType>();
// 原本的作法是使用 React 本身的 createContext,並透過 as 來指定 initialState 的型別
// export const RGBContext = React.createContext<RGBContextType>(
// initialState as RGBContextType
// );
// const Provider = RGBContext.Provider;
export const RGBContextProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
// STEP 4:在 provider 中使用 useReducer,取得 reducer 的資料
const [rgb, dispatch] = React.useReducer(reducer, {
red: 0,
green: 0,
blue: 0,
});
return (
<Provider
value={{
...rgb,
dispatch,
}}
>
{children}
</Provider>
);
};
export interface RGBColorType {
red: number;
green: number;
blue: number;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment