Poznajemy hook useReducer w React i z TypeScriptem. Jeżeli przerobiliśmy poprzednie lekcje, to nie muszę nic więcej tłumaczyć, do dzieła zatem!

Ok, initState, typy akcji, typ akcja:

const initState = { count: 0, text: '' }

const enum REDUCER_ACTION_TYPE {
    INCREMENT,
    DECREMENT,
    NEW_INPUT,
}

type ReducerAction = {
    type: REDUCER_ACTION_TYPE,
    payload?: string,
}

Payload nieobowiązkowy. Teraz funkcja reducer:

const reducer = (state: typeof initState, action: ReducerAction): typeof initState => {
    switch (action.type) {
        case REDUCER_ACTION_TYPE.INCREMENT:
            return { ...state, count: state.count + 1 }
        case REDUCER_ACTION_TYPE.DECREMENT:
            return { ...state, count: state.count - 1 }
        case REDUCER_ACTION_TYPE.NEW_INPUT:
            return { ...state, text: action.payload ?? "" }
        default:
            throw new Error()
    }
}

Dwie lekcje omawialiśmy jak to działa, więc w telegraficznym skrócie:

  • Przyjmuje obiekt o typie wnioskowanym z initState
  • Przyjmuje action typu ReducerAction
  • Zwraca taki sam obiekt o type wnioskowanym z initState
  • Funkcja upewnia się, że nie mutujemy obiektu, tylko zwracamy jego zmodyfikowaną kopię
  • Funkcja osiąga to przez spread starego obiektu i nadpisanie interesującego nas pola
  • W przypadku payload, które jest opcjonalne, ale próbuje nadpisać nam coś, co nie może być undefined, zabezpieczamy się, aby nigdy undefined nie przypisać do typu string

Ok, zaczynamy pisać komponent:

const CounterRed = () => {
    const [state, dispatch] = useReducer(reducer, initState)
//(...)
}
export default CounterRed;

Swoją drogą mam nadzieję, że zaimportowaliśmy useReducer. Mam też nadzieję, że tamte treści trzymamy powyżej poziomu RFC. Ok, w odróżnieniu od useState, useReducer dobrze napisany nie wymaga od nas podawania typów, tu się wszystko samo wnioskuje.

Dopiszmy sobie jsx (tsx) abyśmy wiedzieli, dokąd zmierzamy:

return (
        <>
            <h1>Current count: {state.count}</h1>
            <div>
                <button onClick={increment}>+</button>
                <button onClick={decrement}>-</button>
            </div>
            <input type="text" onChange={handleTextInput} />
            <h2>{state.text}</h2>
        </>
    )

To teraz tylko pozostaje nam napisać te funkcje, które korzystają z dispatch:

const increment = () => dispatch({ type: REDUCER_ACTION_TYPE.INCREMENT })
    const decrement = () => dispatch({ type: REDUCER_ACTION_TYPE.DECREMENT })
    const handleTextInput = (e: ChangeEvent<HTMLInputElement>) => {
        dispatch({
            type: REDUCER_ACTION_TYPE.NEW_INPUT,
            payload: e.target.value
        })
    }

Mam nadzieję, że w tej lekcji się wszystko nam pięknie złożyło do kupy, oto wynik:

const CounterRed = () => {
    const [state, dispatch] = useReducer(reducer, initState)

    const increment = () => dispatch({ type: REDUCER_ACTION_TYPE.INCREMENT })
    const decrement = () => dispatch({ type: REDUCER_ACTION_TYPE.DECREMENT })
    const handleTextInput = (e: ChangeEvent<HTMLInputElement>) => {
        dispatch({
            type: REDUCER_ACTION_TYPE.NEW_INPUT,
            payload: e.target.value
        })
    }

    return (
        <>
            <h1>Current count: {state.count}</h1>
            <div>
                <button onClick={increment}>+</button>
                <button onClick={decrement}>-</button>
            </div>
            <input type="text" onChange={handleTextInput} />
            <h2>{state.text}</h2>
        </>
    )
}
export default CounterRed;

Proste. Więcej TSa i Reacta niedługo!