避免由于其他组件使用的自定义挂钩而重新渲染组件?

Avoid re-rendering of a component because of a custom hook which is used by other components?

我在不同的组件中使用这个自定义的 http 挂钩。

function useHttp(requestFunction, startWithPending = false) {  
  const httpState = useSelector((state) => state.http);
  const dispatch = useDispatch();
  
  const sendRequest = useCallback(

    async function (requestData) {

      dispatch(httpActions.loading(startWithPending));

      try {

        const responseData = await requestFunction(requestData);
        dispatch(httpActions.success(responseData));

      } catch (error) {

        dispatch(httpActions.error(error.message || "Something went wrong!!!"));
      }
    },
    [dispatch, requestFunction, startWithPending]
  );

  return {
    sendRequest,
    ...httpState,
  };
}

我正在使用 Redux 工具包管理状态(我的 http 操作)。

const initialHttpState = {
  status: null,
  data: null,
  error: null,
};

const httpSlice = createSlice({
  name: 'http',
  initialState: initialHttpState,
  reducers: {
    loading(state, action){
      state.status = action.payload ? 'pending' : 'null';
      state.data = null;
      state.error = null;
    },
    success(state, action){
      state.status = 'completed';
      state.data = action.payload;
      state.error = 'null'
    },
    error(state, action){
      state.status = 'error';
      state.data = null;
      state.error = action.payload;
    }
  }
})

我在多个组件的 useEffect 中使用这个钩子:

const { sendRequest, data, status, error } = useHttp(getProducts, true);

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

我的问题是因为不同的组件使用相同的挂钩,每当我尝试在一个组件内调用这个自定义挂钩时,使用这个相同挂钩的任何其他组件都会重新呈现,这会导致我的应用程序崩溃(可以' t read properties of undefined error) 因为如你所见,我正在将响应数据覆盖到同一个地方,而且状态也在改变。
我怎样才能避免这个重新渲染的问题?在我的组件内部,如何检查此重新渲染需求是来自其他组件还是来自组件本身?或者我怎样才能改变我的状态管理结构来处理这个问题?

您可以为每个请求使用一个键并将其发送到负载中以更新切片中的特定键

这里有一个例子:

const initialHttpState = {
  status: null,
  data: null,
  error: null,
};

const httpSlice = createSlice({
name: 'http',
initialState: {},
reducers: {
  loading(state, action){
    if(!state[action.payload.key]) state[action.payload.key] = initialHttpState;
    
    state[action.payload.key].status = action.payload.startWithPending ? 'pending' : 'null';
    state[action.payload.key].data = null;
    state[action.payload.key].error = null;
  },
  success(state, action){
    state[action.payload.key].status = 'completed';
    state[action.payload.key].data = action.payload.responseData;
    state[action.payload.key].error = 'null'
  },
  error(state, action){
    state[action.payload.key].status = 'error';
    state[action.payload.key].data = null;
    state[action.payload.key].error = action.payload.error;
  }
 }
})

在钩子中你将传递密钥:

function useHttp(requestFunction, startWithPending = false, key) {  
  const httpState = useSelector((state) => state.http[key]);
  const dispatch = useDispatch();
  
  const sendRequest = useCallback(

    async function (requestData) {

      dispatch(httpActions.loading({key, startWithPending}));

      try {

        const responseData = await requestFunction(requestData);
        dispatch(httpActions.success({key, responseData}));

      } catch (error) {

        dispatch(httpActions.error({key, error: error.message || "Something went wrong!!!"}));
      }
    },
    [dispatch, requestFunction, startWithPending]
  );

  return {
    sendRequest,
    ...httpState,
  };
}

因此在使用这个钩子时你将添加一个键:

const { sendRequest, data, status, error } = useHttp(getProducts, true, 'getProducts');

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

现在您的 http 状态应该如下所示:

{
  getProducts: { //http state here },
  getUsers: { // http state here }
  ...
}

并且由于状态中的更新针对的是已知键而不是整个状态,因此它不应重新呈现您的组件

PS: :这是问题的答案,但是功能强大的库已经存在,它们具有额外的功能和性能,例如 rtk queryreact-query

在这种情况下,使用单个全局状态 并不是您想要从钩子中得到的。每个 useHttp 挂钩都应该有自己的内部状态,用于管理请求并卸载处理任何响应值的责任。这是从 Redux 代码到使用 useReducer 钩子的相当简单的转换,因此每个 useHttp 钩子都有自己的状态和缩减器逻辑。

示例:

const initialHttpState = {
  status: null,
  data: null,
  error: null,
};

const reducer = (state, action) => {
  switch(action.type) {
    case "loading":
      return {
        status: action.payload ? 'pending': null,
        data: null,
        error: null,
      };

    case "success":
      return {
        ...state,
        status: 'completed',
        data: action.payload,
        error: null,
      };

    case "error":
      return {
        ...state,
        status: 'error',
        data: null,
        error: action.payload,
      };

    default:
      return state;
  }
};

...

const useHttp = (requestFunction, startWithPending = false) => {
  const [httpState, dispatch] = React.useReducer(reducer, initialHttpState);

  const sendRequest = useCallback(
    async function (requestData) {
      dispatch(httpActions.loading(startWithPending));

      try {
        const responseData = await requestFunction(requestData);
        dispatch(httpActions.success(responseData));
      } catch (error) {
        dispatch(httpActions.error(error.message || "Something went wrong!!!"));
      }
    },
    [dispatch, requestFunction, startWithPending]
  );

  return {
    sendRequest,
    ...httpState,
  };
}