简体   繁体   中英

useReducer and useEffect hooks to fetch data

I am experimenting with react hooks, but I am getting a bit confused on how to use useEffect and useReducer .

I have a simple page with few checkboxes, on change of each checkbox I should re-query the api to load new data.

My idea was to code the component as follow:

export const Page: React.FunctionComponent = () => {
  // state to manage the list returned by the api
  const [list, setList] = useState<any[]>([]);
  // this should manage the state related to the checkboxes
  const [checkboxesState, dispatch] = useReducer(reducer, initialCheckboxesState);

  useEffect(() => {
    // load data from api
    loadProducts(checkboxesState);
  }, [checkboxesState]); // my idea here is that when checkboxesState changes, it should trigger the 'useEffect' hook
}

Now regarding the useReducer

// initial state
const initialFiltersState: CheckboxesState = {
  country: [], // array of strings for countries ie ['USA', 'Brazil', 'India']
};

function reducer(
  checkboxesState: CheckboxesState,
  action: { type: string; value: string; checked: boolean }
) {
  const index = checkboxesState[action.type].indexOf(action.value);
  // if checkbox is ticked and it's not already present in the array, insert it
  if (action.checked && index === -1) {
    checkboxesState[action.type].push(action.value);
  } else if (!action.checked && index > -1) {
    // if it's present and we are unchecking it, remove it from the array
    checkboxesState[action.type].splice(index, 1);
  }

  // TODO: this is not the best way to handle reload. I don't want to do it here, this should just trigger the useEffect
  loadProducts(productFilters);
  return checkboxesState;
}

// this is the handler for checkbox changes
const onChangeHandler = (event: any): void => {
  dispatch({
    type: event.target.name, // eg. country
    value: event.target.value, // eg. Australia
    checked: event.target.checked,// bool
  });
};

Now I know it might be something stupid I am doing, but I have been stuck on this for quite a while. Any idea why the useEffect is not called when the checkboxesState changes? Or am I completely off track?

I have tried to pass JSON.stringify(checkboxesState) to useEffect, but no luck either.

My component has a bit more stuff in it that are not relevant, so I tried to put here only the code relevant to the specific task. I Hope I explained myself clearly

You are not updating the state correctly.

Reason why useEffect is not triggered when the state is updated is because you are mutating the state directly . You are returning the same state object from the reducer. So, as far as React is concerned, reducer function doesn't updates the state when it is called. As a result, useEffect is not triggered again.

You need to return a new state object from the reducer so that React knows that state has been updated by the reducer. Modify your reducer to return a new object every time it updates the state.

Another solution could be to use thunk actions, these actions always have access to most recent state and can dispatch multiple actions. Thunk actions can even call other thunk actions and wait for them (if the thunk action returns a promise): thunkAction(arg)(dispatch,getState).then(thunkActionResult=>otherStuff) .

Thunks are well documented and easily recognized by other developers.

Thunk is a middleware of Redux but can be applied to useReducer with a custom hook:

 const { useRef, useState } = React; //custom hook, you can define in different file const compose = (...fns) => fns.reduce((result, fn) => (...args) => fn(result(...args)) ); const mw = () => (next) => (action) => next(action); const createMiddleware = (...middlewareFunctions) => ( store ) => compose( ...middlewareFunctions .concat(mw) .reverse() .map((fn) => fn(store)) ); const useMiddlewareReducer = ( reducer, initialState, middleware = () => (b) => (c) => b(c) ) => { const stateContainer = useRef(initialState); const [state, setState] = useState(initialState); const dispatch = (action) => { const next = (action) => { stateContainer.current = reducer( stateContainer.current, action ); return setState(stateContainer.current); }; const store = { dispatch, getState: () => stateContainer.current, }; return middleware(store)(next)(action); }; return [state, dispatch]; }; //middleware const thunkMiddleWare = ({ getState, dispatch }) => ( next ) => (action) => typeof action === 'function' ? action(dispatch, getState) : next(action); const logMiddleware = ({ getState }) => (next) => ( action ) => { console.log('in log middleware', action, getState()); Promise.resolve().then(() => console.log('after action:', action.type, getState()) ); return next(action); }; //your actions const init = { value: 'A' }; const TOGGLE = 'TOGGLE'; const later = () => new Promise((r) => setTimeout(() => r(), 500)); const otherThunk = () => (dispatch, getState) => { dispatch({ type: 'not relevant action form otherTunk' }); console.log('in other thunk, state is:', getState()); //note that I am returning a promise return later(); }; const thunkToggle = () => (dispatch, getState) => { //dispatching other thunk and waiting for it to finish otherThunk()(dispatch, getState).then(() => dispatch({ type: TOGGLE }) ); }; //your component code const reducer = (state, { type }) => { console.log(`in reducer action type: ${type}`); //toggle state.value between A and B if (type === TOGGLE) { return { value: state.value === 'A' ? 'B' : 'A' }; } return state; }; const middleware = createMiddleware( thunkMiddleWare, logMiddleware ); const App = () => { const [state, dispatch] = useMiddlewareReducer( reducer, init, middleware ); return ( <div> <button onClick={() => dispatch(thunkToggle())}> toggle </button> <pre>{JSON.stringify(state, undefined, 2)}</pre> </div> ); }; ReactDOM.render(<App />, document.getElementById('root'));
 <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script> <div id="root"></div>

With thunk you can move the logic out of your component and just do dispatch(fetchData(args))

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM