1. ホーム
  2. reactjs

[解決済み] React useReducer 非同期データフェッチ

2022-12-01 01:49:17

質問

新しいreact useReducer APIでデータを取得しようとしているのですが、非同期で取得する必要がある段階で行き詰ってしまいました。私はちょうど方法がわからない:/。

どのようにデータフェッチをswitchステートメントに配置するか、それはそれが行われるべき方法ではないのですか?

import React from 'react'

const ProfileContext = React.createContext()

const initialState = {
  data: false
}

let reducer = async (state, action) => {
  switch (action.type) {
    case 'unload':
      return initialState
    case 'reload':
      return { data: reloadProfile() } //how to do it???
  }
}


const reloadProfile = async () => {
  try {
    let profileData = await fetch('/profile')
    profileData = await profileData.json()

    return profileData
  } catch (error) {
    console.log(error)
  }
}

function ProfileContextProvider(props) {
  let [profile, profileR] = React.useReducer(reducer, initialState)

  return (
    <ProfileContext.Provider value={{ profile, profileR }}>
      {props.children}
    </ProfileContext.Provider>
  )
}

export { ProfileContext, ProfileContextProvider }

こんな感じでやろうとしたんですが、asyncでうまくいきません;(

let reducer = async (state, action) => {
  switch (action.type) {
    case 'unload':
      return initialState
    case 'reload': {
      return await { data: 2 }
    }
  }
}

どのように解決するのですか?

それは リデューサを純粋に保つ . そうすることで useReducer をより予測しやすくし、テストしやすくします。その後のアプローチは、どちらも非同期操作と純粋なreducersを組み合わせています。

1. 前にデータを取得する dispatch (単純)

オリジナルをラップする dispatchasyncDispatch で、コンテキストがこの機能を受け継ぐようにします。

const AppContextProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initState);
  const asyncDispatch = () => { // adjust args to your needs
    dispatch({ type: "loading" });
    fetchData().then(data => {
      dispatch({ type: "finished", payload: data });
    });
  };
  
  return (
    <AppContext.Provider value={{ state, dispatch: asyncDispatch }}>
      {children}
    </AppContext.Provider>
  );
  // Note: memoize the context value, if Provider gets re-rendered more often
};

const reducer = (state, { type, payload }) => {
  if (type === "loading") return { status: "loading" };
  if (type === "finished") return { status: "finished", data: payload };
  return state;
};

const initState = {
  status: "idle"
};

const AppContext = React.createContext();

const AppContextProvider = ({ children }) => {
  const [state, dispatch] = React.useReducer(reducer, initState);
  const asyncDispatch = () => { // adjust args to your needs
    dispatch({ type: "loading" });
    fetchData().then(data => {
      dispatch({ type: "finished", payload: data });
    });
  };

  return (
    <AppContext.Provider value={{ state, dispatch: asyncDispatch }}>
      {children}
    </AppContext.Provider>
  );
};

function App() {
  return (
    <AppContextProvider>
      <Child />
    </AppContextProvider>
  );
}

const Child = () => {
  const val = React.useContext(AppContext);
  const {
    state: { status, data },
    dispatch
  } = val;
  return (
    <div>
      <p>Status: {status}</p>
      <p>Data: {data || "-"}</p>
      <button onClick={dispatch}>Fetch data</button>
    </div>
  );
};

function fetchData() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(42);
    }, 2000);
  });
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>

2. ミドルウェアを利用した dispatch (汎用)

dispatch で強化されるかもしれません。 ミドルウェア のように リデュースサンク , redux-observable , redux-saga を追加することで、より柔軟で再利用性の高いものにすることができます。あるいは を書くか を一つ作成します。

例えば、1.) 非同期でデータを取得するために redux-thunk 2.) ロギングをする 3.) dispatch を呼び出します。まず、ミドルウェアを定義します。

import thunk from "redux-thunk";
const middlewares = [thunk, logger]; // logger is our own implementation

次に、カスタム useMiddlewareReducer フックを書きます。 useReducer は、Redux のような追加のミドルウェアとバンドルされています。 applyMiddleware :

const [state, dispatch] = useMiddlewareReducer(middlewares, reducer, initState);

ミドルウェアは第一引数として渡され、それ以外のAPIは useReducer . 実装では applyMiddleware ソースコード を作成し、React Hooksに引き継ぎます。

const middlewares = [ReduxThunk, logger];

const reducer = (state, { type, payload }) => {
  if (type === "loading") return { ...state, status: "loading" };
  if (type === "finished") return { status: "finished", data: payload };
  return state;
};

const initState = {
  status: "idle"
};

const AppContext = React.createContext();

const AppContextProvider = ({ children }) => {
  const [state, dispatch] = useMiddlewareReducer(
    middlewares,
    reducer,
    initState
  );
  return (
    <AppContext.Provider value={{ state, dispatch }}>
      {children}
    </AppContext.Provider>
  );
};

function App() {
  return (
    <AppContextProvider>
      <Child />
    </AppContextProvider>
  );
}

const Child = () => {
  const val = React.useContext(AppContext);
  const {
    state: { status, data },
    dispatch
  } = val;
  return (
    <div>
      <p>Status: {status}</p>
      <p>Data: {data || "-"}</p>
      <button onClick={() => dispatch(fetchData())}>Fetch data</button>
    </div>
  );
};

function fetchData() {
  return (dispatch, getState) => {
    dispatch({ type: "loading" });
    setTimeout(() => {
      // fake async loading
      dispatch({ type: "finished", payload: (getState().data || 0) + 42 });
    }, 2000);
  };
}

function logger({ getState }) {
  return next => action => {
    console.log("state:", JSON.stringify(getState()), "action:", JSON.stringify(action));
    return next(action);
  };
}

// same API as useReducer, with middlewares as first argument
function useMiddlewareReducer(
  middlewares,
  reducer,
  initState,
  initializer = s => s
) {
  const [state, setState] = React.useState(initializer(initState));
  const stateRef = React.useRef(state); // stores most recent state
  const dispatch = React.useMemo(
    () =>
      enhanceDispatch({
        getState: () => stateRef.current, // access most recent state
        stateDispatch: action => {
          stateRef.current = reducer(stateRef.current, action); // makes getState() possible
          setState(stateRef.current); // trigger re-render
          return action;
        }
      })(...middlewares),
    [middlewares, reducer]
  );

  return [state, dispatch];
}

//                                                         |  dispatch fn  |
// A middleware has type (dispatch, getState) => nextMw => action => action
function enhanceDispatch({ getState, stateDispatch }) {
  return (...middlewares) => {
    let dispatch;
    const middlewareAPI = {
      getState,
      dispatch: action => dispatch(action)
    };
    dispatch = middlewares
      .map(m => m(middlewareAPI))
      .reduceRight((next, mw) => mw(next), stateDispatch);
    return dispatch;
  };
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/redux-thunk/2.3.0/redux-thunk.min.js" integrity="sha256-2xw5MpPcdu82/nmW2XQ6Ise9hKxziLWV2GupkS9knuw=" crossorigin="anonymous"></script>
<script>var ReduxThunk = window.ReduxThunk.default</script>

注:中間状態を ミュータブル参照 - stateRef.current = reducer(...) で、各ミドルウェアは起動時に現在の最新の状態にアクセスすることができます。 getState .

を持たせるために 正確 という API を useReducer と同じであれば、Hookを動的に作成することができます。

const useMiddlewareReducer = createUseMiddlewareReducer(middlewares); //init Hook
const MyComp = () => { // later on in several components
  // ...
  const [state, dispatch] = useMiddlewareReducer(reducer, initState);
}

const middlewares = [ReduxThunk, logger];

const reducer = (state, { type, payload }) => {
  if (type === "loading") return { ...state, status: "loading" };
  if (type === "finished") return { status: "finished", data: payload };
  return state;
};

const initState = {
  status: "idle"
};

const AppContext = React.createContext();

const useMiddlewareReducer = createUseMiddlewareReducer(middlewares);

const AppContextProvider = ({ children }) => {
  const [state, dispatch] = useMiddlewareReducer(
    reducer,
    initState
  );
  return (
    <AppContext.Provider value={{ state, dispatch }}>
      {children}
    </AppContext.Provider>
  );
};

function App() {
  return (
    <AppContextProvider>
      <Child />
    </AppContextProvider>
  );
}

const Child = () => {
  const val = React.useContext(AppContext);
  const {
    state: { status, data },
    dispatch
  } = val;
  return (
    <div>
      <p>Status: {status}</p>
      <p>Data: {data || "-"}</p>
      <button onClick={() => dispatch(fetchData())}>Fetch data</button>
    </div>
  );
};

function fetchData() {
  return (dispatch, getState) => {
    dispatch({ type: "loading" });
    setTimeout(() => {
      // fake async loading
      dispatch({ type: "finished", payload: (getState().data || 0) + 42 });
    }, 2000);
  };
}

function logger({ getState }) {
  return next => action => {
    console.log("state:", JSON.stringify(getState()), "action:", JSON.stringify(action));
    return next(action);
  };
}

function createUseMiddlewareReducer(middlewares) {
  return (reducer, initState, initializer = s => s) => {
    const [state, setState] = React.useState(initializer(initState));
    const stateRef = React.useRef(state); // stores most recent state
    const dispatch = React.useMemo(
      () =>
        enhanceDispatch({
          getState: () => stateRef.current, // access most recent state
          stateDispatch: action => {
            stateRef.current = reducer(stateRef.current, action); // makes getState() possible
            setState(stateRef.current); // trigger re-render
            return action;
          }
        })(...middlewares),
      [middlewares, reducer]
    );
    return [state, dispatch];
  }
}

//                                                         |  dispatch fn  |
// A middleware has type (dispatch, getState) => nextMw => action => action
function enhanceDispatch({ getState, stateDispatch }) {
  return (...middlewares) => {
    let dispatch;
    const middlewareAPI = {
      getState,
      dispatch: action => dispatch(action)
    };
    dispatch = middlewares
      .map(m => m(middlewareAPI))
      .reduceRight((next, mw) => mw(next), stateDispatch);
    return dispatch;
  };
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/redux-thunk/2.3.0/redux-thunk.min.js" integrity="sha256-2xw5MpPcdu82/nmW2XQ6Ise9hKxziLWV2GupkS9knuw=" crossorigin="anonymous"></script>
<script>var ReduxThunk = window.ReduxThunk.default</script>

その他の情報 - 外部ライブラリ。 react-use , react-hooks-global-state , react-enhanced-reducer-hook