为什么在useReducer hook中使用switch语句来管理状态?

Why is switch statement used in useReducer hook to manage state?

让我们看看下面两种使用useReducer钩子进行状态管理的方法,它们都做同样的事情:点击添加按钮到+1,点击减去按钮到-1:

  1. 带开关:

const myReducer = (state, action) => {
    switch (action.type) {
        case 'add':
            return {
                count: state.count + 1
            }
        case 'subtract':
            return {
                count: state.count - 1
            }
        default:
            return state
    }
}

const Reducer = () => {
    const [state, dispatch] = useReducer(myReducer, { count: 0 });

    return (
        <>
            <button onClick={() => dispatch({ type: 'add' })}>Add</button>
            <button onClick={() => dispatch({ type: 'subtract' })}>Subtract</button>
            <p>{state.count}</p>
        </>
    )
}

  1. 没有开关

const Reducer2 = () => {
    const [state, setState] = useReducer(
        (state, newState) => ({ ...state, ...newState }),
        { count: 0 }
    );
    
    return (
        <>
            <button onClick={() => setState({count: state.count + 1})}>Add</button>
            <button onClick={() => setState({count: state.count - 1})}>Subtract</button>
            <p>{state.count}</p>
        </>
    )

}

哪一个是更好的状态管理方式?我更喜欢 2,因为它更简单,允许我们以 'class component' 的方式管理状态。我不明白为什么需要 1:它需要一个复杂的 switch 语句;如果要添加状态,则需要一个新案例。这一切看起来很麻烦。

编辑:我知道这是一个简单的例子,不需要使用 useReduceruseState 更好,但我真正想讨论的是,当有多个状态时,一个更好?

Switch 语句通常在 useReducer 中用作 redux 中 reducer 的残余。

你的第二个例子是在函数组件中使用 this.setState 的近似值的好方法,因为 useState 只是真正为单个值设计的,因为没有旧的浅合并状态和新的。我在这个答案的末尾将其进一步扩展了一步。

至于您提出的哪个最适合在 useReducer 中管理状态的问题,这实际上取决于您要使用它做什么以及如何使用它。您不仅限于这两种类型的东西:您可以使用它们中的任何东西。我很幸运地在 useReducer 中使用 redux toolkit 的 createSlice 为带有 Immer 的 type-safe reducer 简化了不可变性。

I don't understand why 1 is needed: it needs a switch statement which is complex; if one wants to add state, a new case is needed

如果你为状态的每个部分写一个reducer case,是的。这非常麻烦,我肯定会用不同的方式来做。使用第一种方法的最佳方法是当您需要处理更复杂的情况或使用通用方法处理更多状态选项时。

React docs中所写:

useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. useReducer also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.

它们是对功能组件的非常 强大的补充,并允许以更简单的方式处理逻辑上连接的复杂逻辑或值。你是否使用它当然取决于你,用 useReducer 完成的任何事情都可以用 useStates 完成,具有不同数量的样板和逻辑。

关于使用大量状态属性的通用方法:

const { useRef, useReducer } = React;
const dataReducer = (state, action) => {
  switch (action.type) {
    case 'toggle':
      return {
        ...state,
        [action.name]: !state[action.name],
      };
    case 'change':
      return {
        ...state,
        [action.name]: action.value,
      };
    default:
      return state;
  }
};
function Example() {
  const [data, dispatch] = useReducer(dataReducer, {
    check1: false,
    check2: false,
    check3: false,
    input1: '',
    input2: '',
    input3: '',
  });
  const throwErrorRef = useRef(null);
  const handleChange = function (e) {
    const { name, value } = e.currentTarget;
    dispatch({ type: 'change', name, value });
  };
  const handleToggle = function (e) {
    const { name } = e.currentTarget;
    dispatch({ type: 'toggle', name });
  };
  const checkBoxes = ['check1', 'check2', 'check3'];
  const inputs = ['input1', 'input2', 'input3'];
  return (
    <div>
      {checkBoxes.map((name) => (
        <label>
          {name}
          <input
            type="checkbox"
            name={name}
            onChange={handleToggle}
            checked={data[name]}
          />
        </label>
      ))}
      <br />
      {inputs.map((name) => (
        <label>
          {name}
          <input
            type="text"
            name={name}
            onChange={handleChange}
            value={data[name]}
          />
        </label>
      ))}
    </div>
  );
}

ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"/>

至于稍微复杂的逻辑,这里有一个数据获取的例子:

const { useRef, useReducer } = React;
const dataReducer = (state, action) => {
  switch (action.type) {
    case 'fetchStart':
      return {
        loading: true,
        data: null,
        error: null,
      };
    case 'fetchError':
      if (!state.loading) {
        return state;
      }
      return {
        loading: false,
        data: null,
        error: action.payload.error,
      };
    case 'fetchSuccess': {
      if (!state.loading) {
        return state;
      }
      return {
        loading: false,
        data: action.payload.data,
        error: null,
      };
    }
    default:
      return state;
  }
};
function Example() {
  const [{ loading, data, error }, dispatch] = useReducer(dataReducer, {
    loading: false,
    data: null,
    error: null,
  });
  const throwErrorRef = useRef(null);
  const handleFetch = function () {
    if (loading) {
      return;
    }
    dispatch({ type: 'fetchStart' });
    const timeoutId = setTimeout(() => {
      dispatch({ type: 'fetchSuccess', payload: { data: { test: 'Text' } } });
    }, 5000);
    throwErrorRef.current = () => {
      clearTimeout(timeoutId);
      dispatch({ type: 'fetchError', payload: { error: 'Oh noes!' } });
    };
  };
  const handleFetchError = function () {
    throwErrorRef.current && throwErrorRef.current();
  };
  return (
    <div>
      <button onClick={handleFetch}>Start Loading</button>
      <button onClick={handleFetchError}>Throw an error in the fetch!</button>
      <div>loading: {`${loading}`}</div>
      <div>error: {error}</div>
      <div>data: {JSON.stringify(data)}</div>
    </div>
  );
}

ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"/>

我用过的一个简单的方法是强制更新,它只是增加一个值以使组件 re-render。

const [,forceUpdate] = useReducer((state)=>state+1,0);
// Example use: forceUpdate();

我修改了您的示例 2 以添加对更新状态的函数方法的支持,因此它更接近使用 useReducer 的完整 setState 仿制品。我想不出一个合适的方法来使回调工作(this.setState 中的第二个参数)

const { useRef, useReducer } = React;
const stateReducer = (state, action) => {
  if (typeof action === 'function') {
    action = action(state);
  }
  return { ...state, ...action };
};
const useMergeState = (initialState) => {
  return useReducer(stateReducer, initialState);
};
function Example() {
  const [state, setState] = useMergeState({
    loading: false,
    data: null,
    error: null,
    count: 0,
  });
  const throwErrorRef = useRef(null);
  const handleFetch = function () {
    if (state.loading) {
      return;
    }
    setState({ loading: true });
    const timeoutId = setTimeout(() => {
      setState({
        data: { text: 'A super long text', loading: false, error: null },
      });
    }, 5000);
    throwErrorRef.current = () => {
      clearTimeout(timeoutId);
      setState({ error: 'Oh noes!', loading: false, data: null });
    };
  };
  const handleFetchError = function () {
    throwErrorRef.current && throwErrorRef.current();
  };
  const incrementCount = function () {
    setState((state) => ({ count: state.count + 1 }));
    setState((state) => ({ count: state.count + 1 }));
  };
  return (
    <div>
      <button onClick={handleFetch}>Start Loading</button>
      <button onClick={handleFetchError}>Throw an error in the fetch!</button>
      <div>loading: {`${state.loading}`}</div>
      <div>error: {state.error}</div>
      <div>data: {JSON.stringify(state.data)}</div>
      <button onClick={incrementCount}>increase count by 2</button>
      <div>count: {state.count}</div>
    </div>
  );
}

ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"/>