使用 Immer 进行 React 状态管理

Avatar of Kingsley Silas
Kingsley Silas

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 价值 200 美元的免费积分!

我们使用 状态 来跟踪应用程序数据。 状态会随着用户与应用程序的交互而改变。 发生这种情况时,我们需要更新显示给用户的状态,我们使用 React 的 setState 来实现

由于状态不应直接更新(因为 React 的状态必须是不可变的),因此随着状态变得更加复杂,事情会变得非常复杂。 它们变得难以理解和跟踪。

这就是 Immer 出现的地方,这也是我们将在本文中探讨的内容。 使用 Immer,状态可以简化并更容易跟踪。 Immer 使用了一种称为“草稿”的东西,您可以将其视为您的状态副本,而不是状态本身。 就像 Immer 在状态上按下了 CMD+C 然后在某个安全的地方按下了 cmd+V,这样就可以安全地查看而不会干扰原始副本。 您需要进行的任何更新都会发生在草稿上,并且当前状态中草稿上发生更改的部分会得到更新。

假设您的应用程序状态如下;

this.state = {
  name: 'Kunle',
  age: 30,
  city: 'Lagos,
  country: 'Nigeria'
}

这位用户碰巧正在庆祝他的 31 岁生日,这意味着我们需要更新年龄值。 在 Immer 在后台运行的情况下,将创建此状态的副本。

现在想象一下,副本被制作出来并交给了信使,信使将状态的新副本交给 Kunle。 这意味着现在有两个副本可用 - 当前状态和交给 Kunle 的草稿副本。 然后,Kunle 将草稿上的年龄更改为 31。 然后信使带着草稿回到应用程序,比较两个版本,只更新年龄,因为这是草稿中唯一更改的部分。

它并没有破坏不可变状态的概念,因为当前状态不会直接更新。 Immer 基本上使使用不可变状态变得很方便。

让我们看一个实际的例子

假设您想为您的社区建造一个交通灯,您可以尝试使用 Immer 来更新您的状态。

查看 CodePen 上的
使用 Reactjs 的交通灯示例
,由 CarterTsai (@CarterTsai) 创建。
CodePen 上。

使用 Immer,该组件将如下所示

const {produce} = immer

class App extends React.Component {
  state = {
    red: 'red', 
    yellow: 'black', 
    green: 'black',
    next: "yellow"
  }

  componentDidMount() {
    this.interval = setInterval(() => this.changeHandle(), 3000);
  }
  
  componentWillUnmount()  {
    clearInterval(this.interval);
  }

  handleRedLight = () => {
    this.setState(
      produce(draft => {
        draft.red = 'red';
        draft.yellow = 'black';
        draft.green = 'black';
        draft.next = 'yellow'
      })
    )
  }
  
  handleYellowLight = () => {
    this.setState(
      produce(draft => {
        draft.red = 'black';
        draft.yellow = 'yellow';
        draft.green = 'black';
        draft.next = 'green'
      })
    )
  }
  
  handleGreenLight = () => {
    this.setState(
      produce(draft => {
        draft.red = 'black';
        draft.yellow = 'black';
        draft.green = 'green';
        draft.next = 'red'
      })
    )
  }

  changeHandle = () => {
    if (this.state.next === 'yellow') {
      this.handleYellowLight()
    } else if (this.state.next === 'green') {
      this.handleGreenLight()
    } else {
      this.handleRedLight()
    }
    
  }

  render() {
    return (
      <div className="box">
        <div className="circle" style={{backgroundColor: this.state.red}}></div>
        <div className="circle" style={{backgroundColor: this.state.yellow}}></div>
        <div className="circle" style={{backgroundColor: this.state.green}}></div>
      </div>
  );
}
};

produce 是我们从 Immer 中获得的默认函数。 在这里,我们将其作为值传递给 setState() 方法。 produce 函数接受一个函数,该函数接受 draft 作为参数。 正是在这个函数中,我们可以设置草稿副本,我们希望用它来更新我们的状态。

如果看起来很复杂,还有另一种方法可以写这个。 首先,我们创建一个函数。

const handleLight = (state) => {
  return produce(state, (draft) => {
    draft.red = 'black';
    draft.yellow = 'black';
    draft.green = 'green';
    draft.next = 'red'
  });
}

我们将应用程序的当前状态和接受 draft 作为参数的函数传递给 produce 函数。 为了在我们的组件中使用它,我们执行以下操作;

handleGreenLight = () => {
  const nextState = handleLight(this.state)
  this.setState(nextState)
}

另一个例子:购物清单

如果您已经使用 React 一段时间了,那么您并不陌生于 扩展运算符。 使用 Immer,您无需使用扩展运算符,尤其是在处理状态中的数组时。

让我们通过创建一个购物清单应用程序来进一步探索它。

查看 CodePen 上的
immer 2 - 购物清单
,由 Kingsley Silas Chijioke (@kinsomicrote) 创建。
CodePen 上。

这是我们正在使用的组件

class App extends React.Component {
  constructor(props) {
      super(props)
      
      this.state = {
        item: "",
        price: 0,
        list: [
          { id: 1, name: "Cereals", price: 12 },
          { id: 2, name: "Rice", price: 10 }
        ]
      }
    }

    handleInputChange = e => {
      this.setState(
      produce(draft => {
        draft[event.target.name] = event.target.value
      }))
    }

    handleSubmit = (e) => {
      e.preventDefault()
      const newItem = {
        id: uuid.v4(),
        name: this.state.name,
        price: this.state.price
      }
      this.setState(
        produce(draft => {
          draft.list = draft.list.concat(newItem)
        })
      )
    };

  render() {
    return (
      <React.Fragment>
        <section className="section">
          <div className="box">
            <form onSubmit={this.handleSubmit}>
              <h2>Create your shopping list</h2>
              <div>
                <input
                  type="text"
                  placeholder="Item's Name"
                  onChange={this.handleInputChange}
                  name="name"
                  className="input"
                  />
              </div>
              <div>
                <input
                  type="number"
                  placeholder="Item's Price"
                  onChange={this.handleInputChange}
                  name="price"
                  className="input"
                  />
              </div>
              <button className="button is-grey">Submit</button>
            </form>
          </div>
          
          <div className="box">
            {
              this.state.list.length ? (
                this.state.list.map(item => (
                  <ul>
                    <li key={item.id}>
                      <p>{item.name}</p>
                      <p>${item.price}</p>
                    </li>
                    <hr />
                  </ul>
                ))
              ) : <p>Your list is empty</p>
            }
          </div>
        </section>
      </React.Fragment>
    )
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

随着项目被添加到清单中,我们需要更新清单的状态以反映这些新项目。 要使用 setState() 更新 list 的状态,我们必须这样做

handleSubmit = (e) => {
  e.preventDefault()
  const newItem = {
    id: uuid.v4(),
    name: this.state.name,
    price: this.state.price
  }
  this.setState({ list: [...this.state.list, newItem] })
};

如果您必须更新应用程序中的多个状态,则必须进行大量的扩展以使用旧状态和附加值创建新状态。 随着更改次数的增加,这看起来可能会更加复杂。 使用 Immer,这变得非常容易,就像我们在上面的示例中所做的那样。

如果我们想添加一个函数,该函数在状态更新之后被调用作为回调? 在这种情况下,假设我们正在跟踪清单中项目的数量和所有项目的总价格。

查看 CodePen 上的
immer 3 - 购物清单
,由 Kingsley Silas Chijioke (@kinsomicrote) 创建。
CodePen 上。

假设我们想根据清单中项目的價格计算总支出,我们可以让 handleSubmit 函数如下所示

handleSubmit = (e) => {
  e.preventDefault()
  const newItem = {
    id: uuid.v4(),
    name: this.state.name,
    price: this.state.price
  }
  
  this.setState(
    produce(draft => {
      draft.list = draft.list.concat(newItem)
    }), () => {
      this.calculateAmount(this.state.list)
    }
  )
};

首先,我们使用用户输入的数据创建一个对象,然后将其分配给 newItem。 为了更新我们应用程序的状态,我们使用 .concat(),它将返回一个由之前项目和新项目组成的新的数组。 此更新的副本现在被设置为 draft.list 的值,然后 Immer 可以使用它来更新应用程序的状态。

回调函数在状态更新后被调用。 重要的是要注意它使用的是更新后的状态。

我们想要调用的函数将如下所示

calculateAmount = (list) => {
  let total = 0;
    for (let i = 0; i < list.length; i++) {
      total += parseInt(list[i].price, 10)
    }
  this.setState(
    produce(draft => {
      draft.totalAmount = total
    })
  )
}

让我们看看 Immer 挂钩

use-immer 是一个挂钩,允许您在 React 应用程序中管理状态。 让我们使用经典的计数器示例来看看它的实际作用。

import React from "react";
import {useImmer} from "use-immer";

const Counter = () => {
  const [count, updateCounter] = useImmer({
    value: 0
  });

  function increment() {
    updateCounter(draft => {
      draft.value = draft.value +1;
    });
  }

  return (
    <div>
      <h1>
        Counter {count.value}
      </h1>
      <br />
      <button onClick={increment}>Increment</button>
    </div>
  );
}

export default Counter;

useImmer 类似于 useState。 该函数返回状态和一个更新器函数。 当组件首次加载时,状态的值(在本例中为 count)与传递给 useImmer 的值相同。 使用返回的更新器函数,我们可以创建一个 increment 函数来增加计数的值。

还有一个 useReducer 样式的 Immer 挂钩。

import React, { useRef } from "react";
import {useImmerReducer } from "use-immer";
import uuidv4 from "uuid/v4"
const initialState = [];
const reducer = (draft, action) => {
  switch (action.type) {
    case "ADD_ITEM":
      draft.push(action.item);
      return;
    case "CLEAR_LIST":
      return initialState;
    default:
      return draft;
  }
}
const Todo = () => {
  const inputEl = useRef(null);
  const [state, dispatch] = useImmerReducer(reducer, initialState);
  
  const handleSubmit = (e) => {
    e.preventDefault()
    const newItem = {
      id: uuidv4(),
      text: inputEl.current.value
    };
    dispatch({ type: "ADD_ITEM", item: newItem });
    inputEl.current.value = "";
    inputEl.current.focus();
  }
  
  const handleClear = () => {
    dispatch({ type: 'CLEAR_LIST' })
  }
  
  return (
    <div className='App'>
      <header className='App-header'>
        <ul>
          {state.map(todo => {
            return <li key={todo.id}>{todo.text}</li>;
          })}
        </ul>
        <form onSubmit={handleSubmit}>
          <input type='text' ref={inputEl} />
          <button
            type='submit'
          >
            Add Todo
          </button>
        </form>
        <button
          onClick={handleClear}
        >
          Clear Todos
        </button>
      </header>
    </div>
  );
}
export default Todo;

useImmerReducer 接收一个 reducer 函数和初始状态,并返回状态和分派函数。 然后,我们可以遍历状态以显示我们拥有的项目。 我们在提交待办事项项目并清除其列表时分派一个操作。 分派的 action 具有一个类型,我们使用它来确定在 reducer 函数中要做什么。
在 reducer 函数中,我们像以前一样使用 draft,而不是 state。 通过这样做,我们有一个方便的方法来操作我们应用程序的状态。

您可以在 GitHub 上找到上述示例中使用的代码。

这就是 Immer 的概览!

展望未来,您可以在下一个项目中开始使用 Immer,甚至可以逐步开始在您正在进行的当前项目中使用它。 它已被证明有助于使状态管理变得方便。