useReducer, useContext 함께 사용하기

Jin·2022년 3월 1일
0

React

목록 보기
2/17
import React from "react";
import Helmet from "react-helmet";
import Loader from "../Components/Loader";
import Message from "../Components/Message";
import { useDetail } from "../hooks/useDetail";
import Info from "../Components/Info";
import { useDetailState } from "../contexts/DetailContext";

function Detail() {
    useDetail();
    const { loading, error } = useDetailState();

    return loading ? (
        <>
            <Helmet>
                <title>Loading | Jimmyflix</title>
            </Helmet>
            <Loader />
        </>
    ) : (
        error ? (
            <Message color="#e74c3c" text={error}></Message>
        ) : (
            <Info />
        )
    )
}

export default Detail;
import { FAIL, SUCCESS } from "../actions";

export const detailInitialState = {
    result: null,
    error: null,
    loading: true
};

const detailReducer = (state, action) => {
    switch (action.type) {
        case SUCCESS:
            return {
                ...state,
                result: action.payload,
                loading: false
            }
        case FAIL:
            return {
                result: null,
                error: "Can't find Detail information.",
                loading: false,
            }
        default:
            return;
    }
}

export default detailReducer;

reducer는 state를 변경하는 함수들의 모음이라고 생각하시면 됩니다.
모음이라 함은 당연히 여러 경우의 수가 있다는 뜻이겠죠. 여러 경우의 수가 존재하지 않는다면 useState로 state를 관리하시는 편이 훨씬 이로울 것입니다.
reducer 함수에서 return하는 값들은 기존의 state를 완전히 대체하게 될 것입니다. 이 점에 유의하여야 합니다.
reduer.js에서는 초기값에 해당하는 initialState와 reducer 함수를 export해줍니다.

import { createContext, useContext, useReducer } from "react";
import detailReducer, { detailInitialState } from "../reducers/DetailReducer";

const DetailContext = createContext();

const DetailProvider = ({ children }) => {
    const [state, dispatch] = useReducer(detailReducer, detailInitialState);

    return (
        <DetailContext.Provider value={{ state, dispatch }}>
            {children}
        </DetailContext.Provider>
    )
}

export const useDetailDispatch = () => {
    const { dispatch } = useContext(DetailContext);
    return dispatch;
}

export const useDetailState = () => {
    const { state } = useContext(DetailContext);
    return state;
}


export default DetailProvider;

context.js 코드입니다.
DetailProvider 안의 코드를 보시게 되면 useReducer의 파라미터로 reducer 함수와 initialState가 선언됩니다.
그러면 받게 되는 값은 상태에 해당하는 state와 상태를 변경할 수 있는 함수인 dispatch입니다.
이 변수들을 provider의 value로 주게 되면

<Router>
        <>
            <Header />
            <Switch>
                <Route path="/" exact>
                    <HomeProvider>
                        <Home />
                    </HomeProvider>
                </Route>
                <Route path="/tv">
                    <TVProvider>
                        <TV />
                    </TVProvider>
                </Route>
                <Route path="/search">
                    <SearchProvider>
                        <Search />
                    </SearchProvider>
                </Route>
                <Route path={["/movie/:id", "/show/:id"]}>
                    <DetailProvider>
                        <Detail />
                    </DetailProvider>
                </Route>
                <Route path="*">
                    <HomeProvider>
                        <Redirect to={{ Home }} />
                    </HomeProvider>
                </Route>
            </Switch>
        </>
    </Router>

Home, TV, Detail, Search 모두 제가 만든 컴포넌트들입니다.

이런 식으로 Provider안에 들어가게 되는 것들이 children에 해당되게 되고 그 안에 있는 것들은 모두 Provider의 value로 준 값들에 접근할 수 있게 됩니다.

import { useEffect } from "react";
import useRouter from "use-react-router";
import { FAIL, SUCCESS } from "../actions";
import { moviesApi, tvApi } from "../api";
import { useDetailDispatch } from "../contexts/DetailContext";

export function useDetail() {
    const { match: { params: { id } }, location: { pathname }, history: { push } } = useRouter();
    const dispatch = useDetailDispatch();
    const isMovie = pathname.includes("/movie/");

    async function getDetail() {
        const parsedId = parseInt(id);

        if (isNaN(parsedId)) {
            return push("/");
        }

        let results;
        try {
            if (isMovie) {
                ({ data: results } = await moviesApi.movieDetail(parsedId));
            } else {
                ({ data: results } = await tvApi.showDetail(parsedId));
            }
            console.log(results);
            dispatch({ type: SUCCESS, payload: results });
        } catch {
            dispatch({ type: FAIL });
        }
    }

    useEffect(() => {
        window.scrollTo(0, 0);
        getDetail();
    }, [id]);
}

이제 우리는 useDetail이라는 hook을 만들어서 Detail 컴포넌트 안에서 호출하게 할 것입니다.
그러면 이미 DetailContext에서 state, dispatch를 주었으므로 useDetailDispatch() 이런 식으로 호출만 하면 됩니다.
그러고 로직의 적정한 지점에 dispatch를 호출해주면 코드가 기존보다 훨씬 깔끔해짐을 느낄 수 있습니다.

마지막으로 이 useDetail을 선언하는 Detail 컴포너트 부분의 코드입니다.

import React from "react";
import Helmet from "react-helmet";
import Loader from "../Components/Loader";
import Message from "../Components/Message";
import { useDetail } from "../hooks/useDetail";
import Info from "../Components/Info";
import { useDetailState } from "../contexts/DetailContext";

function Detail() {
    useDetail();
    const { loading, error } = useDetailState();

    return loading ? (
        <>
            <Helmet>
                <title>Loading | Jimmyflix</title>
            </Helmet>
            <Loader />
        </>
    ) : (
        error ? (
            <Message color="#e74c3c" text={error}></Message>
        ) : (
            <Info />
        )
    )
}

export default Detail;

Detail 컴포넌트에서는 그저 useDetail을 호출하고 useDetailState에서 값을 받는 것뿐입니다.

이렇게 useContext와 useReducer를 함께 사용하면 기존의 props로 넘기는 부분이 필요가 없게 되고 일일이 useState를 쓰지 않게 되기 때문에 여러 state와 변경할 여러 경우의 수가 있을 때 적극 추천합니다.

그렇다고 너무 남용하게 되면 코드의 흐름을 읽을 데에 오히려 독이 될 수 있기에 어떤 state가 '최소 2개 이상의 컴포넌트를 거쳐야 할 때에 context provider를 만들어 사용한다' 같은 본인의 기준을 두시고 그에 맞춰 사용하심이 좋아보입니다.

profile
배워서 공유하기

0개의 댓글