使用useReducer手动获取数据

时间:2020-03-03 15:55:42

标签: reactjs react-hooks

不仅在ComponentDidMount上,而且在方法loadData()onFiltersClearonApplyFilters之后,我都需要使用onPageChange从服务器获取数据。我该怎么办?我是否需要在此处添加一些自定义数据抓取钩子,或者还有其他方法可以做到这一点?

import { Table } from "lpcnse-core-front";
import Filters from "./Filters/Filters";
import { styles } from "./checks.css";

const STATUSES_PARENT_ID = "2F24B71C-C70A-4980-9D45-E4134D38E9E0";

const ITEMS_PER_PAGE = 20;

const INITIAL_PAGINATION = {
  pageNumber: 1,
  itemsPerPageCount: ITEMS_PER_PAGE,
  totalCount: 0,
  pageCount: 0
};

const INITIAL_FILTERS = {
  title: "",
  сontrolledObject: "",
  status: null
};

const initialState = {
  filters: INITIAL_FILTERS,
  checks: [],
  pagination: INITIAL_PAGINATION,
  selectedCheckId: null,
  statusList: []
};

const reducer = (state, action) => {
  switch (action.type) {
    case "FETCH_DATA_PENDING":
      return {
        ...state,
        checks: []
      };
    case "FETCH_DATA_SUCCESS":
      const { items, pageNumber, pageCount, totalCount } = action.payload;
      return {
        ...state,
        checks: items,
        pagination: {
          ...state.pagination,
          pageCount,
          totalCount,
          pageNumber
        }
      };
    case "FETCH_DATA_ERROR":
      return {
        ...state,
        checks: [],
        pagination: INITIAL_PAGINATION
      };
    case "SET_FILTER_INPUT":
      return {
        ...state,
        filters: {
          ...state.filters,
          ...action.payload
        }
      };
    case "SET_FILTER_STATE":
      const { status } = action.payload;
      return {
        ...state,
        filters: {
          ...state.filters,
          status
        }
      };
    case "CLEAR_FILTERS":
      return {
        ...state,
        filters: INITIAL_FILTERS
      };
    case "SET_PAGE_NUMBER":
      return {
        ...state,
        pagination: {
          ...state.pagination,
          pageNumber
        }
      };
    case "RESET_PAGINATION":
      return {
        ...state,
        pagination: INITIAL_PAGINATION
      };
    case "FETCH_STATUSES_SUCCESS":
      const { statusList } = action.payload;
      return {
        ...state,
        statusList
      };
    case "FETCH_STATUSES_ERROR":
      return {
        ...state,
        statusList: []
      };
    default:
      throw new Error();
  }
};

export const Checks = props => {
  const [state, dispatch] = useReducer(reducer, initialState);

  const {
    checks,
    pagination: { pageCount, pageNumber },
    filters,
    filters: { title, сontrolledObject, status },
    selectedCheckId,
    statusList
  } = state;

  useEffect(() => {
    loadData();
    loadStatusList();
  }, []);

  const loadData = async () => {
    try {
      const response = await props.api.check.getChecks({
        filters: {
          title,
          сontrolledObject,
          status
        },
        pageNumber,
        maxRowCount: ITEMS_PER_PAGE
      });

      const { items, pageNumber, pageCount, totalCount } = response.data;
      dispatch({
        type: "FETCH_DATA_SUCCESS",
        payload: {
          pageNumber,
          pageCount,
          totalCount,
          items: items || []
        }
      });
    } catch (error) {
      dispatch({
        type: "FETCH_DATA_ERROR"
      });
    }
  };

  const loadStatusList = async () => {
    try {
      const response = await props.api.type.getStatuses({
        parentId: STATUSES_PARENT_ID
      });
      const { items } = response.data;
      dispatch({
        type: "FETCH_STATUSES_SUCCESS",
        payload: {
          statusList: items || []
        }
      });
    } catch (error) {
      dispatch({
        type: "FETCH_STATUSES_ERROR"
      });
    }
  };

  const onFilterInputChange = e => {
    dispatch({
      type: "SET_FILTER_INPUT",
      payload: {
        [e.target.name]: e.target.value
      }
    });
  };

  const onFilterStatusChange = e => {
    dispatch({
      type: "SET_FILTER_STATE",
      payload: {
        status
      }
    });
  };

  const onFiltersClear = () => {
    dispatch({
      type: "CLEAR_FILTERS"
    });

    // loadData
  };

  const onApplyFilters = () => {
    dispatch({
      type: "RESET_PAGINATION"
    });

    // loadData
  };

  const onPageChange = data => {
    dispatch({
      type: "SET_PAGE_NUMBER",
      payload: {
        pageNumber: data.selected + 1
      }
    });

    // loadData
  };

  const onSort = () => {};

  const onOpenUserCard = () => {};

  const columns = [
    //some columns
  ];

  return (
    <div className="checks">
      <Filters
        onFilterInputChange={onFilterInputChange}
        onFilterStatusChange={onFilterStatusChange}
        onApplyFilters={onApplyFilters}
        onClear={onFiltersClear}
        filters={filters}
        statusList={statusList}
      />
      <Table
        columns={columns}
        data={checks}
        onSort={onSort}
        paginated={true}
        onPageChange={onPageChange}
        pageCount={pageCount}
        forcePage={pageNumber}
        selectedItemId={selectedCheckId}
      />

      <style jsx>{styles}</style>
    </div>
  );
};

3 个答案:

答案 0 :(得分:3)

使加载函数响应过滤器更改的关键是将过滤器添加到useEffect()依赖项列表中。

useEffect(() => {
  loadData();
  loadStatusList();
}, [filters, pageNumber]);

尽管我看不到loadStatusList()取决于过滤器,所以建议将两个异步调用分成各自的效果

useEffect(() => {
  loadData();
}, [filters, pageNumber]);

useEffect(() => {
  loadStatusList();
}, []);

最后,在每个渲染器上都将重新创建load *函数,因此您可以将它们移动到各自的效果中,或将它们包装在useCallback()中,或将它们移动到组件外部(但那样一来,传递所有参数,而不使用闭包。

useEffect(() => {

  const loadData = async () => {
    ...
  });

  loadData();
}, [filters]);

useEffect(() => {

  const loadStatusList = async () => {
    ...
  });

  loadStatusList();
}, []);

从评论中我可以看到您尚未关联事件(单击按钮)并且状态发生了变化。

  1. 副作用,尤其是异步的 必须 useEffect()包装。

  2. useEffects是在 状态更改时触发的(或特殊情况,即空依赖列表=== onMount)。

  3. 由于只能在状态更改上运行效果,因此必须使事件(单击按钮)更改某些状态,然后使用useEffect()依赖项列表中的该状态作为触发器。

因此,在onFilterInputChange事件之后

onFilterInputChange 
-> dispatch(SET_FILTER_INPUT) 
-> reducer sets new value for filters
-> effect has filters as dependency, so it runs
-> loadData called with new filters as parameter
-> on response dispatch(FETCH_DATA_SUCCESS) with new data

我们可能唯一的问题是pagerNumber是效果的输入和输出,因此在更新时可能会得到反馈循环。

为了安全起见,我们可以添加一个控制获取的状态变量

const initialState = {
  filters: INITIAL_FILTERS,
  checks: [],
  pagination: INITIAL_PAGINATION,
  selectedCheckId: null,
  statusList: [],
  doFetch: true    // initially true === do a fetch on mount
};

const reducer = (state, action) => {
  switch (action.type) {

    case "FETCH_DATA_PENDING":
      return {
        ...state,
        checks: [],
        doFetch: true
      };    

    case "FETCH_DATA_SUCCESS":
      const { items, pageNumber, pageCount, totalCount } = action.payload;
      return {
        ...state,
        checks: items,
        pagination: {
          ...state.pagination,
          pageCount,
          totalCount,
          pageNumber
        },
        doFetch: false
      };

    case "FETCH_DATA_ERROR":
      return {
        ...state,
        checks: [],
        pagination: INITIAL_PAGINATION,
        doFetch: false
      };
    ...

  const onFilterInputChange = e => {
    dispatch({
      type: "SET_FILTER_INPUT",
      payload: {
        [e.target.name]: e.target.value
      }
    });
    dispatch({type: "FETCH_DATA_PENDING" });
  };

  const onFilterStatusChange = e => {
    dispatch({
      type: "SET_FILTER_STATE",
      payload: {
        status
      }
    });
    dispatch({type: "FETCH_DATA_PENDING" });
  };

  const onFiltersClear = () => {
    dispatch({
      type: "CLEAR_FILTERS"
    });
    dispatch({type: "FETCH_DATA_PENDING" });
  };


useEffect(() => {
  if (doFetch) {
    loadData();
  }
}, [doFetch]);

答案 1 :(得分:1)

您可以使用参考手动触发效果。当使用useReducer不会产生副作用时,这通常很有用。

 let fireFiltersClearEffect = React.useRef(false);

 const onFiltersClear = () => {
    dispatch({
      type: "CLEAR_FILTERS"
    });

    fireFiltersClearEffect.current = true;
  };

 React.useEffect(() => {
   if (fireFiltersClearEffect.current !== false) {
      loadData();
      fireFiltersClearEffect.current = false;
   }
  });

答案 2 :(得分:0)

另一种解决方案是使用 thunk 动作,这些动作总是可以访问最近的状态并且可以分派多个动作。 Thunk 动作甚至可以调用其他 thunk 动作并等待它们(如果 thunk 动作返回承诺):thunkAction(arg)(dispatch,getState).then(thunkActionResult=>otherStuff).

Thunk 有很好的文档记录并且很容易被其他开发者识别。

Thunk 是 Redux 的中间件,但可以通过自定义钩子应用于 useReducer:

const { useRef, useState } = React;

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);
};

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 })
  );
};
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>