简体   繁体   English

如何在没有 redux 的反应(使用钩子)的减速器之间共享 state

[英]How to share state between reducers in react (using hooks) without redux

I have a react hooks application written in typescript that contains multiple reducers, and I am using them with the context API.我有一个用 typescript 编写的 react hooks 应用程序,其中包含多个减速器,我将它们与上下文 API 一起使用。

I need to share the error state between reducers, because I need only one error state displayed in the app, which can be cleared/set with the errorReducer .我需要在减速器之间共享错误 state,因为我只需要在应用程序中显示一个错误 state,可以使用errorReducer清除/设置。

The trouble is, I need to set the error part of the state from the 'other' reducers (by 'other' I mean not the errorReducer ).问题是,我需要从“其他”减速器设置 state 的错误部分(“其他”我的意思不是errorReducer )。

If I try to use the useReducer hook (or my own useAsyncReducer hook) inside the 'other' reducers to set an error, I get如果我尝试在“其他”减速器中使用 useReducer 钩子(或我自己的useAsyncReducer钩子)来设置错误,我会得到

Error: Invalid hook call.错误:无效的挂钩调用。 Hooks can only be called inside of the body of a function component钩子只能在 function 组件的主体内部调用

, as you can see below. ,如下所示。

How can I share state between reducers in react?如何在 React 中的减速器之间共享 state? (please see the 'TODO: THIS IS WHAT I NEED' below). (请参阅下面的“待办事项:这是我需要的”)。

Please note that I do not want to use redux.请注意,我不想使用 redux。

export type Actor = {
    _id?: string,
    firstName?: string,
    lastName?: string,
    selectedForDelete?: boolean
}

// Compound state for 3 reducers (activeEntityTypeReducer, actorReducer, errorReducer)
export type State = {
    activeEntityType: string,
    actors: Actor[],
    error: string | null
}

export const EMPTY_INITIAL_STATE: State = {
    activeEntityType: EntityType.ACTOR,
    actors: [],
    error: null
};


// ERROR REDUCER:
export const errorReducer = async (state: string | null, action: ErrorActions) => {
    switch (action.type) {
        case ErrorActionType.SET_ERROR: {
            return action.payload;
        }

        default:
            return state;
    }
};

// ACTOR REDUCER:
export const actorReducer = async (state: Actor[], action: ActorActions) => {
  // I cannot use here a hook like this because it triggers: "Error: Invalid hook call. Hooks can only be called inside of the body of a function component"
  // const { dispatch: dispatchError } = useAsyncReducer(errorReducer, EMPTY_INITIAL_STATE);
  switch (action.type) {
    //... other actions

    case ActorActionType.SEARCH_ACTORS: {
      return fetch(
        "http://localhost:3000/api/actors?pagesize=100"
      ).then(response => response.json())
        .then(response => response['data']);
      /*  // TODO: THIS IS WHAT I NEED: continue the above line with a catch inside which I dispatch the error
      .catch((error) => dispatchError({
        type: ErrorActionType.SET_ERROR,
        payload: error
      }))
      */
    }

    default:
      return state;
  }
};

// MAIN (COMPOSED) REDUCER:
export const mainReducer = async ({ activeEntityType, actors, error }: State, 
    action: ActiveEntityTypeActions | ActorActions | ErrorActions) => (
    {
        actors: await actorReducer(actors, action as ActorActions),
        activeEntityType: activeEntityTypeReducer(activeEntityType, action as ActiveEntityTypeActions),
        // more reducers here and all need to set the error
        error: await errorReducer(error, action as ErrorActions)
    });

Use context to store error state in Parent component, then use useContext to access state stored in context.使用 context 将错误 state 存储在 Parent 组件中,然后使用useContext访问存储在 context 中的 state。 Because you need components to update state in context, you include a method for updating state - I don't use Typescript因为您需要组件在上下文中更新 state,所以您包括更新 state 的方法 - 我不使用 Typescript

 const ErrorContext = React.createContext({
  error: null,
  setError: err => {
    this.error = err;
  }
});

let ComponentOne = () => {
  const [link, setLink] = useState("");
  const errorState = useContext(ErrorContext);

  // http fetching is an impure action, it does not belong in reducers
  // in redux we place asnc actions in middlewares such as thunk and saga

  let fetchLink = () => {
    fetch(link)
      .then(res => res.json())
      .then(response => {
        // if data, pass action to reducer to update state
        console.log("++ found something: ", response);
      })
      .catch(exc => {
        console.error("++ exc: ", exc);
        // here we use context to set error state
        errorState.setError("From Component1: " + exc.message);
      });
  };
  return (
    <>
      <input type="text" name="link" onChange={e => setLink(e.target.value)} />
      <button onClick={fetchLink}>fetch content from ComponentTwo</button>
    </>
  );
};

let ComponentTwo = () => {
  const [link, setLink] = useState("");
  const errorState = useContext(ErrorContext);

  // http fetching is an impure action, it does not belong in reducers
  // in redux we place asnc actions in middlewares such as thunk and saga

  let fetchLink = () => {
    fetch(link)
      .then(res => res.json())
      .then(response => {
        // if data, pass action to reducer to update state
        console.log("++ found something: ", response);
      })
      .catch(exc => {
        console.error("++ exc: ", exc);
        // here we use context to set error state
        errorState.setError("From Component2: " + exc.message);
      });
  };
  return (
    <>
      <input type="text" name="link" onChange={e => setLink(e.target.value)} />
      <button onClick={fetchLink}>fetch content from ComponentTwo</button>
    </>
  );
};

export default function App() {
  const [error, setError] = useState("");
  return (
    <ErrorContext.Provider
      value={{
        error,
        setError
      }}
    >
      <div className="App">
        <h1>Error: {error}</h1>
        <hr />
        <ComponentOne />
        <br />
        <ComponentTwo />
      </div>
    </ErrorContext.Provider>
  );
}

Also, API calls do not belong in reducers, best do all async calls inside component and emit actions when you have results or encounter exceptions.此外,API 调用不属于 reducer,最好在组件内部执行所有异步调用,并在有结果或遇到异常时发出操作。

Note: I have a codesandbox注意:我有一个代码框

I found a solution:我找到了一个解决方案:

I changed the actorReducer to update the error part of the state as well.我更改了actorReducer以更新 state 的error部分。 This means that actorReducer receives now both the actors and the error parts of the state and returns both of them.这意味着actorReducer现在同时接收到state 的actorserror部分并返回它们。

For simplicity, I renamed the action names to start with words that identify the reducers, so that I can easily decide to which reducer to forward an action in the mainReducer (which is the combined reducer) by just checking the prefix of an incoming action (so ActorActionType.SEARCH_ACTORS was renamed to ActorActionType.ACTOR__SEARCH , etc).为简单起见,我将动作名称重命名为以标识减速器的单词开头,这样我就可以通过检查传入动作的mainReducer (所以ActorActionType.SEARCH_ACTORS被重命名为ActorActionType.ACTOR__SEARCH等)。

Here is a part of the code:这是代码的一部分:

type State = {
    activeEntityType: string,
    actors: Actor[],
    error: string | null
}

// For actorReducer
enum ActorActionType {
  ACTOR__ADD = 'ACTOR__ADD',
  ACTOR__SELECT_FOR_DELETING = 'ACTOR__SELECT_FOR_DELETING',
  ACTOR__DELETE = 'ACTOR__DELETE',
  ACTOR__SEARCH = 'ACTOR__SEARCH'
}

// For errorReducer
enum ErrorActionType {
    ERROR__SET = 'ERROR__SET',
}

// for activeEntityTypeReducer
const ACTIVE_ENTITY_TYPE__SET: string = "ACTIVE_ENTITY_TYPE__SET";

const actorReducer = async (actors: Actor[], error: string | null, action: ActorActions) => {
  case ActorActionType.ACTOR__SEARCH: {
    // fetch and error handling and fill newActors and newOrOldError
    return { actors: newActors, error: newOrOldError};
  }
}

// The combined (main) reducer:
const mainReducer = async ({ activeEntityType, actors, error }: State, 
    action: ActiveEntityTypeActions | ActorActions | ErrorActions) => {
        if (action.type.startsWith('ACTOR__')) {
            const actorsAndError = (await actorReducer(actors, error, action as ActorActions));
            return {...actorsAndError, activeEntityType};
        }

        if (action.type.startsWith('ACTIVE_ENTITY_TYPE__')) {
            // No need for now to pass in and return the error for activeEntityTypeReducer, as it can never error
            return {activeEntityType: activeEntityTypeReducer(activeEntityType, action as ActiveEntityTypeActions), actors, error};
        }

        // Can only be the errorReducer case:
        return {activeEntityType, actors, error: await errorReducer(error, action as ErrorActions)};
};

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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