React state 结构设计原则

1,081 阅读4分钟

React State 结构设计

React 中组件 state 状态管理是组件设计中的难点之一,如何设计state的结构。遵循以下原则可以保障state更新不出现逻辑上的错误,也可以避免不必要的 state 维护:

相关的state组合成一个group

当每次触发更新的时候需要更新两个state 则这两个state可以尝试合并成一个state【从单个值类型,变成object 或者 Array 等类型】。

import { useState } from 'react';

function ComA() {
  // bad case 
  const [x, setX] = useState<number>(0);
  const [y, setY] = useState<number>(0);
  
  // good case
  const [position, setPosition] = useState({ x0y0 });
  return (
    <div>
      <div style={{
          position: 'absolute',
          backgroundColor: 'red',
          borderRadius: '50%',
          transform: `translate(${x}px, ${y}px)`,
          left: -10,
          top: -10,
          width: 20,
          height: 20,
        }} />
    </div>
    );
}

避免出现竞态的state

也就是说组件中存在两个或多个 state 存在竞态问题,同一时刻有且仅有一个是真值。如果存在这种问题,则需要考虑避免当前这种state的结构, 使用不同的值去区分冲突的 state,这样就把多个冲突的state 合并成1个state,区别在于value的变化以及其代表的意义。

import { useState } from 'react';

// bad  case
function ComA() {
  // 表示编辑状态
  const [isWritting, setIsWritting] = useState(true);
  // 表示是否保存
  const [isSave, setIsSave] = useState(fasle);
  
  // 其他状态
  const [isComplete, setIsComplete] = useState(false);
  
  return (
    //...
  );
}

// 这中间 isWriting 和 isSave 是冲突的。也就是说两个state存在竞态,有且仅有一个是真值。

// combine mutilate state ingroup 
function ComB() {

  const [status, setStatus] = useState<'writing' | 'save' | 'complete'>('writing');
  
  return (
    // ...
  );
}

避免多余的state

如果一个 state 可以通过其他 state 的计算得出【.length, 取反异或等】,那么这个 state 就是不需要存在的。

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState('');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
    setFullName(e.target.value + ' ' + lastName);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
    setFullName(firstName + ' ' + e.target.value);
  }

  return (
    <>
      <h2>Let’s check you in</h2>
      <label>
        First name:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}


// fullname  完全可以由 firstName 和 lastName 拼接出来,使用单独的 state 来保存计算结果是多余的。

避免重复的状态

如果state存在重复相同的数据时,这部分重复的数据很难保持同步更新。【一般是针对数组项的处理,data 保存在一个state 中,然后又使用一个state保存选中或者编辑某项。这时候data中的数据更新,current 可能会被缓存到旧值】。需要避免这种重复。解决办法【避免保存重复的内容,而是保存找到指定数据的id或者索引】。

import { useState } from 'react';

const defaultData = [
    { title'Tom'id0 },
    { title'Sam'id1 },
    { title'Dodo'id2 },
    { title'Piker'id3 },
];
function ComA() {
    const [data, setData] = useState(defaultData);
    const [current, setCurrent] = useState(data[0]);
  
    function handleClick(item) {
      setCurrent(item);
    }
  
    function handleInput(id, value) {
      const newData = data.map((item)=>{
        if (id === item.id) {
          return { ...item, title: value };
        }
        return item;
      })
    }
  
  return (
    <>
      <ul>
        {
          data.map((item) => {
            return (
              <li key={item.id}>
                <input value={item.title} onChange{({target})=>{ handleInput(item.id, target.value); }}/>
                <button onClick={()=>{ handleClick(item) }}>选中</button>
              </li>
            );
          })
        }
      </ul>
      <p>当前选中:{current.title}</p>
     </>
  );
}


// 问题: 当点击 “选中” 按钮后, current 保存了当前 item 的一个引用。接着编辑当前项的title,发现并不会同步到<p>中展示。

解决方法1: 细心检查代码能看出来,通过 handleInput 执行时,返回了新的对象更新 data 中的 item。只要稍微修改一下handleInput的代码,同时更新current即可。

function handleInput(id, value) {
      const newData = data.map((item)=>{
        if (id === item.id) {
          // return { ...item, title: value };
          const newItem = { ...item, title: value };
          setCurrent(newItem);
          return newItem;
        }
        return item;
      })
    }

但是这种方式不能一劳永逸,其他函数中再修改其他属性数据,还得增加同样的逻辑。

解决方法2:保存 item 的 id 不要保存重复的数据内容。

import { useState } from 'react';

const defaultData = [
    { title'Tom'id0 },
    { title'Sam'id1 },
    { title'Dodo'id2 },
    { title'Piker'id3 },
];
function ComA() {
    const [data, setData] = useState(defaultData);
    // 修改
    const [currentId, setCurrentId] = useState(0);
  
    const currentItem = data.find(({id}) => id === currentId);
  
    function handleClick({id}) {
      setCurrentId(id);
    }
  
    function handleInput(id, value) {
      const newData = data.map((item)=>{
        if (id === item.id) {
          return { ...item, title: value };
        }
        return item;
      })
    }
  
  return (
    <>
      <ul>
        {
          data.map((item) => {
            return (
              <li key={item.id}>
                <input value={item.title} onChange{({target})=>{ handleInput(item.id, target.value); }}/>
                <button onClick={()=>{ handleClick(item) }}>选中</button>
              </li>
            );
          })
        }
      </ul>
      <p>当前选中:{current.title}</p>
     </>
  );
}

一劳永逸解决问题,保存id。更新的时候组件会自动获取对应的数据项

避免出现过深的嵌套state

深度嵌套的state不便于更新,更新时,需要一层一层的解构,重组成新的嵌套对象。如果可以尝试使用平铺的方式组织state结构。react 进行state更新时,引用类型数据需要使用新的引用结构进行更新【解构复制,修改对应value】,如果嵌套层级过多,更新时解构层级越复杂,容易出问题。

以这些原则作为 state 结构设计方法论,逐步实现性感&合理的 React 组件!