如何在没有redux的reactor之间(使用钩子)在reducer之间共享状态

时间:2020-05-27 06:27:52

标签: javascript reactjs typescript react-hooks

我有一个用打字稿编写的react hooks应用程序,其中包含多个reducer,并且将它们与上下文API一起使用。

我需要在化简器之间共享错误状态,因为我只需要在应用程序中显示一个错误状态,即可使用errorReducer清除/设置。

麻烦的是,我需要从“其他”减速器设置状态的错误部分(通过“其他”,我的意思不是errorReducer)。

如果我尝试在“其他”减速器中使用useReducer钩子(或我自己的useAsyncReducer钩子)来设置错误,我会得到

错误:无效的挂钩调用。挂钩只能在身体内部使用 功能组件的说明

,如下所示。

如何在减速器之间共享状态以进行反应?(请参阅下面的“待办事项:这是我需要的”)。

请注意,我不想使用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)
    });

2 个答案:

答案 0 :(得分:0)

使用上下文将错误状态存储在父组件中,然后使用useContext访问存储在上下文中的状态。由于您需要组件来更新上下文中的状态,因此包含了一种更新状态的方法-我不使用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>
  );
}

此外,API调用不属于reducer,最好在组件内进行所有异步调用,并在有结果或遇到异常时发出操作。

注意:我有一个codesandbox

答案 1 :(得分:0)

我找到了解决方法:

我也更改了actorReducer来更新状态的error部分。这意味着actorReducer现在同时接收状态的actorserror部分,并同时返回它们。

为简单起见,我将操作名称重命名为以标识化简器的单词开头,以便通过检查即可轻松确定mainReducer(组合化简器)中将哪个动作转发给哪个归化器。传入操作的前缀(因此ActorActionType.SEARCH_ACTORS重命名为ActorActionType.ACTOR__SEARCH,依此类推)。

这是代码的一部分:

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