【问题标题】:How to prevent non-deterministic state updation in Redux?如何防止 Redux 中的非确定性状态更新?
【发布时间】:2020-05-04 23:56:47
【问题描述】:

使用 Redux 时,保持初始状态的形状至关重要。由于我们无法控制属性,API 调用等副作用的结果/数据将改变状态的形状。例如,考虑这个初始状态:

const book = {
  id: 0,
  name: 'something'
};

book sub-reducer 会根据 API 数据对其进行如下更新:

//receives `book` part of the state
const bookReducer = (state=book, action) => {
   switch(action.type) {
     case 'SET_BOOK': {
       return { ...action.payload };
     } default: 
       return state;
   }
}

可能发生的两种情况:

  1. 如果从 API 发送的数据是 null,,那么新生成的状态现在是 {},这是扩展运算符的结果。如果 UI 的某些部分要监听状态的 book 部分,那么它将中断。可能从 API 数据访问各个属性?在这种情况下,需要对属性执行空/未定义检查。有没有更优雅的解决方案?

  2. 数据中可能还有其他我们可能不感兴趣的属性。可能使用对象映射器来过滤未使用的属性?

处理此类情况并防止状态变得不确定的最佳做法是什么?请分享您如何处理这些场景的经验。

【问题讨论】:

  • 这就是为什么您通常将两个现有状态与即将到来的更新合并。return { ...state, ...action.payload };
  • @GabrielePetrioli 来自 API 的属性值(id 和 name)也可以是 null。在这种情况下,需要执行 null 检查并为属性提供默认值以防止 UI 崩溃。

标签: reactjs redux react-redux


【解决方案1】:

只有 reducer 必须是纯的/确定性的,而不是它之外的东西。

为了防止你的reducer错误地覆盖数据,在API响应和dispatch-call之间写一些逻辑来确保reducer总是得到有效的数据。

例如,thunk 可能如下所示:

const createBook = (name) => {
  return async dispatch => {
   // suppose the api call gives back "uid" plus extra data
   const { uid, ...unneededData } = await myApi.setBook(name);

   // dispatch the data in the way the reducer expects it 
   dispatch({ type: 'SET_BOOK', id: uid, name });
  }
}

在上面的例子中,api调用给了我uid,但没有name,还有一堆额外的数据。只需在发送数据之前准备好数据。

【讨论】:

    【解决方案2】:

    最佳做法是防止应用从各个方面中断,这意味着您需要在从 reducer 返回之前检查和格式化数据。 在您的情况下,我会检查数据的有效性并将其映射到必要的格式。

    1. 仅当 API 响应同时具有 id 和 book 时才调度“SET_BOOK”。
    2. 为了避免不必要的附加属性,您始终可以在调度之前映射您的数据const book = {id: apiData.id, book: apiData.book}

    【讨论】:

      【解决方案3】:

      在您的减速器中,您可以执行以下操作。这样即使响应中有其他键/值,也只有 idname 会得到更新。这也将确保如果收到空值,那么这些值将不会在状态中更新。希望这将有助于解决问题。

          //receives `book` part of the state
          const bookReducer = (state=book, action) => {
            const { type, payload } = action;
             switch(type) {
               case 'SET_BOOK': {
                 return { 
                   ...state,
                   ...(payload.id && {id: payload.id}),
                   ...(payload.name && {name: payload.name})
                 };
               } default: 
                 return state;
             }
          }
      
      

      【讨论】:

        【解决方案4】:

        由于它的确定性,您的 redux reducer 逻辑不应该担心这一点。您在其他地方(redux thunk 或组件)处理您的 api 调用和响应处理,然后调度操作以设置您的 redux。以您的示例为基础:

        book.reducer.js

        const book = {
          id: 0,
          name: ''
        };
        
        const bookReducer = (state=book, action) => {
           switch(action.type) {
             case 'SET_BOOK': {
               return { ...action.payload };
             } default: 
               return state;
           }
        

        book.actions.js

        const setBook = (book) => ({
            type: SET_HEROES,
            payload: book
        });
        
        // thunk
        const findBook = name => async dispatch => {
            const book = await bookService.findBook(name);
        
            if (book) {
                dispatch(setBook(book));
            }
        };
        

        book.service.js

        const findBook = async (name) => {
            // Do your api call
            const bookResponse = axios.get(`${apiUrl}/book/search/${name}`);
        
            // Handle the response
            if (!bookResponse) {
              // Logic you do if book not found
              return null;
            }
        
            return {id: bookResponse.id, name: bookResponse.name};
        }
        

        现在在一个组件中,您可以调度 findBook 调用

        组件.js

            const Component = () => {
                const [search, setSearch] = useState('');
                const dispatch = useDispatch();
        
                const handleOnSearch = () => {
                    dispatch(findBook(search));
                }
        
                return (
                     <div>
                         <input value={search} onChange={(e) => setSearch(e.target.value)}/>
                         <button onClick={handleOnSearch}>Search</button>
                     </div>
                );
            }
        

        【讨论】:

          【解决方案5】:

          如果来自 API 的字段值未定义,则将其转换为 null 并存储,以便代码不会中断和可操作。如果 API 还提供了其他参数,则解构 API 返回的对象并提取所需的字段。这样就可以避免存储不必要的数据。

          const bookReducer = (state=book, action) => {
             switch(action.type) {
               case 'SET_BOOK': {
                  const {id, name, otherParam1, otherParam2} = action.payload;
                  return {
                    id: id || null,
                    name: name || null,
                    otherParam1,
                    otherParam2
                  }
               } default: 
                 return state;
             }
          }
          

          拥有null 值不会破坏代码,它不会呈现任何内容 这比破坏代码的undefined 更好

          希望对你有帮助

          【讨论】:

            【解决方案6】:

            我所做的是将我的所有逻辑都包含在我的操作方法中,并为正确完成操作创建reducer,并为何时拒绝操作创建另一个reducer。在完成的减速器中,我会执行常规指令,而在被拒绝的减速器中,我会将数据添加到一个名为 error 的变量中,该变量始终处于我的状态,并在需要时在前端使用以显示错误消息。

            例子

            这是一个通过向我的 api 发送一个 post 请求来创建房子的操作,如果出现问题,它会返回创建的对象或错误。

            export const createHouse = houseData => {
              const URL = HTTP://EXAMPLE.URL
            
              return async dispatch => {
                try {
                  const response = await axios.post(`${URL}`, houseData);
                  const data = await response.data;
                  dispatch({ type: "CREATE_HOUSE_DRAFT_FULFILLED", data });
                } catch (err) {
                  dispatch({ type: "CREATE_HOUSE_DRAFT_REJECTED", data: err });
                }
              };
            };
            

            然后我将有 2 个 reducer 方法来接收已完成或已拒绝的响应,就像这样。

                    case 'CREATE_HOUSE_DRAFT_FULFILLED': {
                        return {
                            houses: [action.data, ...state.houses],
                            house: action.data,
                            houseCount: state.houseCount + 1,
                            fetched: true,
                            error: null
                        };
                    }
                    case 'CREATE_HOUSE_DRAFT_REJECTED': {
                        return {
                            ...state,
                            error: action.data.response.data,
                            fetched: false,
                            success: null
                        };
                    }
            

            希望这对你有用!

            【讨论】:

              猜你喜欢
              • 2018-04-03
              • 1970-01-01
              • 2019-01-15
              • 2019-07-05
              • 1970-01-01
              • 2020-06-15
              • 1970-01-01
              • 2021-10-29
              • 1970-01-01
              相关资源
              最近更新 更多