【问题标题】:Translating context to redux with setTimeout使用 setTimeout 将上下文转换为 redux
【发布时间】:2021-06-21 09:07:04
【问题描述】:

我有这样的背景:

interface AlertContextProps {
  show: (message: string, duration: number) => void;
}

export const AlertContext = createContext<AlertContextProps>({
  show: (message: string, duration: number) => {
    return;
  },
});

export const AlertProvider: FC<IProps> = ({ children }: IProps) => {
  const [alerts, setAlerts] = useState<JSX.Element[]>([]);

  const show = (message: string, duration = 6000) => {
    let alertKey = Math.random() * 100000;
    setAlerts([...alerts, <Alert message={message} duration={duration} color={''} key={alertKey} />]);
    setTimeout(() => {
      setAlerts(alerts.filter((i) => i.key !== alertKey));
    }, duration + 2000);
  };

  return (
    <>
      {alerts}
      <AlertContext.Provider value={{ show }}>{children}</AlertContext.Provider>
    </>
  );
};

我需要将其“翻译”为 redux 切片。除了show 方法之外,我掌握了一切。正确的治疗方法是什么?我在想一个thunk,但它不是一个thunk。使用 setTimeout 使其成为 reducer 似乎也是一件丑陋的事情。那你们会怎么做呢?

到目前为止我的代码:

type Alert = [];

const initialState: Alert = [];

export const alertSlice = createSlice({
  name: 'alert',
  initialState,
  reducers: {
    setAlertState(state, { payload }: PayloadAction<Alert>) {
      return payload;
    },
  },
});

export const { setAlertState } = alertSlice.actions;
export const alertReducer = alertSlice.reducer;

【问题讨论】:

    标签: javascript reactjs typescript redux react-redux


    【解决方案1】:

    超时是一个副作用,因此您可以在 thunk 中实现它。

    您有一个显示警报消息的操作,其中包含消息、ID 和显示时间的有效负载,当该时间用完时,需要删除警报消息,因此您还需要删除警报消息操作,即从带有警报消息 id 的负载的 thunk 分派。

    我不确定为什么要在时间上增加 2 秒来隐藏消息 duration + 2000,因为调用者可以决定消息应该显示多长时间,我认为它不应该一半忽略该值并随机增加 2 秒。

    这是一个警告消息的 redux 示例:

    const { Provider, useDispatch, useSelector } = ReactRedux;
    const { createStore, applyMiddleware, compose } = Redux;
    
    const initialState = {
      messages: [],
    };
    //action types
    const ADD_MESSAGE = 'ADD_MESSAGE';
    const REMOVE_MESSAGE = 'REMOVE_MESSAGE';
    //action creators
    const addMessage = (id, text, time = 2000) => ({
      type: ADD_MESSAGE,
      payload: { id, text, time },
    });
    const removeMessage = (id) => ({
      type: REMOVE_MESSAGE,
      payload: id,
    });
    //id generating function
    const getId = (
      (id) => () =>
        id++
    )(1);
    const addMessageThunk = (message, time) => (dispatch) => {
      const id = getId();
      dispatch(addMessage(id, message, time));
      setTimeout(() => dispatch(removeMessage(id)), time);
    };
    const reducer = (state, { type, payload }) => {
      if (type === ADD_MESSAGE) {
        return {
          ...state,
          messages: state.messages.concat(payload),
        };
      }
      if (type === REMOVE_MESSAGE) {
        return {
          ...state,
          messages: state.messages.filter(
            ({ id }) => id !== payload
          ),
        };
      }
      return state;
    };
    //selectors
    const selectMessages = (state) => state.messages;
    //creating store with redux dev tools
    const composeEnhancers =
      window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
    const store = createStore(
      reducer,
      initialState,
      composeEnhancers(
        applyMiddleware(
          //simple implementation of thunk (not official redux-thunk)
          ({ dispatch }) =>
            (next) =>
            (action) =>
              typeof action === 'function'
                ? action(dispatch)
                : next(action)
        )
      )
    );
    const App = () => {
      const messages = useSelector(selectMessages);
      const dispatch = useDispatch();
      return (
        <div>
          <button
            onClick={() =>
              dispatch(addMessageThunk('hello world', 1000))
            }
          >
            Add message
          </button>
          <ul>
            {messages.map((message) => (
              <li key={message.id}>{message.text}</li>
            ))}
          </ul>
        </div>
      );
    };
    
    ReactDOM.render(
      <Provider store={store}>
        <App />
      </Provider>,
      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>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
    
    
    <div id="root"></div>

    【讨论】:

      【解决方案2】:

      @HMR 使用 thunk 很好,但我不喜欢他们对减速器所做的事情。你已经在使用 redux-toolkit,这很棒! redux-toolkit 实际上包含并导出了一个nanoid 函数,他们在幕后使用该函数为thunk 创建唯一的ID。你可以用它来代替Math.random() * 100000

      我总是从考虑类型开始。什么是Alert?您不想存储&lt;Alert/&gt;,因为JSX.Element 不可序列化。相反,您应该只存储道具。你肯定会存储messagekey/id。如果您在前端处理过期,那么您还将存储 duration,但如果过期由 thunk 处理,那么我认为您在 redux 状态或组件道具中不需要它。

      您似乎希望一次允许多个警报,因此return payload 不会为您的减速器削减它。您需要存储一个数组或一个键控对象来存储所有活动警报。

      你绝对不应该在减速器中使用setTimeout,因为这是一个副作用。您可以在 thunk 或 useEffect 组件中的 useEffect 中使用它。我倾向于该组件,因为警报似乎也应该可以忽略?因此,您可以使用相同的函数来处理关闭点击和自动超时。

      我们可以定义要为每个警报存储的信息。

      type AlertData = {
        message: string;
        id: string;
        duration: number;
      }
      

      以及我们创建该警报所需的信息,它们是相同的,但没有id,因为我们将在reducer 中生成id。

      type AlertPayload = Omit<AlertData, 'id'>
      

      我们的状态可以是一个警报数组:

      const initialState: AlertData[] = [];
      

      我们需要采取措施来添加新警报并在警报过期后删除它。

      import { createSlice, PayloadAction, nanoid } from "@reduxjs/toolkit";
      ...
      
      export const alertSlice = createSlice({
        name: "alert",
        initialState,
        reducers: {
          addAlert: (state, { payload }: PayloadAction<AlertPayload>) => {
            const id = nanoid(); // create unique id
            state.push({ ...payload, id }); // add to the state
          },
          removeAlert: (state, { payload }: PayloadAction<string>) => {
            // filter the array -- payload is the id
            return state.filter((alert) => alert.id !== payload);
          }
        }
      });
      
      export const { addAlert, removeAlert } = alertSlice.actions;
      export const alertReducer = alertSlice.reducer;
      

      现在到组件了。我的想法是,您将使用选择器来选择所有警报,然后每个警报将负责自己的到期。

      export const AlertComponent = ({ message, duration, id }: AlertData) => {
        const dispatch = useDispatch();
      
        // function called when dismissed, either by click or by timeout
        // useCallback is just so this can be a useEffect dependency and won't get recreated
        const remove = useCallback(() => {
          dispatch(removeAlert(id));
        }, [dispatch, id]);
      
        // automatically expire after the duration, or if this component unmounts
        useEffect(() => {
          setTimeout(remove, duration);
          return remove;
        }, [remove, duration]);
      
        return (
          <Alert
            onClose={remove} // can call remove directly by clicking the X
            dismissible
          >
            <Alert.Heading>Alert!</Alert.Heading>
            <p>{message}</p>
          </Alert>
        );
      };
      
      export const ActiveAlerts = () => {
        const alerts = useSelector((state) => state.alerts);
      
        return (
          <>
            {alerts.map((props) => (
              <AlertComponent {...props} key={props.id} />
            ))}
          </>
        );
      };
      

      我还制作了一个组件来创建警报来测试它并确保它有效!

      export const AlertCreator = () => {
        const dispatch = useDispatch();
      
        const [message, setMessage] = useState("");
        const [duration, setDuration] = useState(8000);
      
        return (
          <div>
            <h1>Create Alert</h1>
            <label>
              Message
              <input
                type="text"
                value={message}
                onChange={(e) => setMessage(e.target.value)}
              />
            </label>
            <label>
              Duration
              <input
                type="number"
                step="1000"
                value={duration}
                onChange={(e) => setDuration(parseInt(e.target.value, 10))}
              />
            </label>
            <button
              onClick={() => {
                dispatch(addAlert({ message, duration }));
                setMessage("");
              }}
            >
              Create
            </button>
          </div>
        );
      };
      
      const App = () => (
        <div>
          <AlertCreator />
          <ActiveAlerts />
        </div>
      );
      export default App;
      

      Code Sandbox Link

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-02-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-05-19
        相关资源
        最近更新 更多