State Management In Pure React, v2
Introduction
This is a course for keeping your state manageable when it’s no longer a toy application.
In this course, we’ll be working with pure React.
So, what are we going to do today?
- Think deeply about what “state” even means in a React application.
- Learn a bit about the inner workings of
this.setState
. - How class-based component state and hooks differ.
- Explore APIs for navigating around prop-drilling.
- Use reducers for advanced state management.
- Write our own custom hooks for managing state.
- Store state in Local Storage.
- Store state in the URL using query parameters.
- Fetch state from a server-because that’s a thing.
And now… Understanding State
The main job of React is to take your application state and turn it into DOM nodes.
- 在之前,我们可能通过JQuery来获取页面上UI所保存的数据并通过对其操作来构建应用状态管理。
- 在一些现代框架中,我们通过一些JS的数据结构来保存应用转态并将其映射到UI中。
- rules(data) => ui
Type of State
There are many kinds of state.
- Model data: The nouns in your application.
- View/UI state: Are those nouns sorted in ascending or descending order?
- Session state: Is the user even logged in?
- Communication: Are we in the process of fetching the nouns from the server?
- Location: Where are we in the application? Which nouns are we looking at?
Or, it might make sense to think about state relative to time.
- Model state: This is likely the data in your application. This could be the items in a given list.
- Ephemeral state: Stuff like the value of an input field that will be wiped away when you hit “enter”. This could be the order in which a given list is sorted.
Spoiler alert: There is no silver bullet.
Class-Based State
setState & Class
And now… An Uncomfortably Close Look at React Component State
Let’s start with the world’s simplest React component.
https://github.com/stevekinney/simple-counter
Oh, wow - it looks like it’s time for a pop quiz, already
1 | class Counter extends Component { |
setState & Asynchronous
1 | this.setState({ count: this.state.count + 1}); |
如果我们在一个方法中多次执行setState对同一个state进行操作,那么打印出来的值是什么?
答案是 0
this.setState()
is asynchronous.React is trying to avoid unnecessary re-renders.
1 | export default class Counter extends Component { |
What will the count be after the user’s clicks the “Increment” button?
- We may expect get “3” but actually get “1”
Effectively, you’re queuing up state changes.
React will batch them up, figure out the result and then efficiently make that change.
实际上述过程可能类似如下代码
1 | Object.assign( |
setState & Function
- There is actually a bit more to
this.setState()
1 | export default class Counter extends Component { |
By use a function in setState, we can get “3” this time.
When you pass functions to
this.setState()
, it plays through each of them.因为传递的是一个回调函数,所以我们可以在函数内进行一些判断或其他操作
1 | export default class Counter extends Component { |
- 我们还可以通过回调的第二个参数获取当前组件的参数对象,所以我们可以进行如下操作
1 | increment() { |
- 又因为回调函数只是一个普通函数,所以我们也可以将其抽离出来以便进行测试。
setState & Callback
this.setState
不仅可以接受一个参数,还可以接受第二个参数作为改变state之后的回调函数
1 | increment() { |
setState & Helper Function
1 | const getStateFromLocalStorage = () => { |
setState Patterns
- Patterns and anti-patterns
- When we’re working with props, we have PropTypes. That’s not the case with state.*
1 | function shouldIKeepSomethingInReactState() { |
Don’t use
this.state
for derivations of props.Bad example
1
2
3
4
5
6
7
8class User extends Component {
constructor(props) {
super(props)
this.state = {
fullName: props.firstName + ' ' + props.lastName
}
}
}Don’t do this. Instead, derive computed properties directly from the props themselves.
Good
1
2
3
4
5
6
7
8
9class User extends Component {
render() {
const { firstName, lastName } = this.props
const fullName = firstName + ' ' + lastName
return (
<h1>{fullName}</h1>
)
}
}Alternatively
1
2
3
4
5
6
7
8
9
10
11
12class User extends Component {
get fullName() {
const { firstName, lastName } = this.props
return firstName + ' ' + lastName
}
render() {
return (
<h1>{this.fullName}</h1>
)
}
}You don’t need to shove everything into your render method.
You can break things out into helper methods
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class UserList extends Component {
render() {
const { users } = this.props
return (
<section>
<VeryImportantUserControls />
{ users.map(user => (
<UserProfile
key={user.id}
photograph={user.mugshot}
onLayoff={handleLayoff}
/>
)) }
<SomeSpecialFooter>
</section>
)
}
}- to
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class UserList extends Component {
renderUserProfile(user) {
return (
<UserProfile
key={user.id}
photograph={user.mugshot}
onLayoff={handleLayoff}
/>
)
}
render() {
const { users } = this.props
return (
<section>
<VeryImportantUserControls />
{ users.map(this.renderUserProfile) }
<SomeSpecialFooter>
</section>
)
}
}- or
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19const renderUserProfile = user => {
return (
<UserProfile
key={user.id}
photograph={user.mugshot}
onLayoff={handleLayoff}
/>
)
}
const UserList = ({users}) => {
return (
<section>
<VeryImportantUserControls />
{ users.map(this.renderUserProfile) }
<SomeSpecialFooter>
</section>
)
}Don’t use state for things you’re not going to render.
Use sensible defaults.
Hooks State
Refactoring & Hooks
- And now…
- An Equally Uncomfortably Close Look at React Hooks
1 | const [count, setCount] = React.useState(0) |
1 | const increment = () => setCount(c => c + 1) |
- 这里与this.setState类似同样可以传入一个函数,注意与class component不同的是必须要有返回值,否则state会变为undefined.
useEffect & Dependencies
1 | useEffect(() => { |
Refactoring & Custom Hook
1 | const useLocalStorage = (initialState, key) => { |
Persisting State & useRef
如果我们在一个类组件的
componentDidUpdate()
生命周期中延时三秒打印this.state.count
, 并同时连续点击按钮使count自增,那么结果会打印出什么?
如果点击七次,那么会输出七次7
说明我们虽然改变了this.state.count的值,但是仍保持了同一个引用
如果我们使用函数组件的useState保存状态,同样在useEffect内延时三秒打印,这次会输出什么?
答案是点击时的数据
这其实是基于JS的基本原理而不是React的独创,使用类组件我们在this.state这一个对象中保存数据,所以this.state永远指向同一对象,那么获取属性时会获取这个对象属性的最新值。而hooks变量保存状态,所以每次改变时都指向不同的值。
那么有时我们需要获取到当前状态和之前状态,例如进行比较或者撤销,显然useState是无法做到的
1 | const Counter = () => { |
useEffect & Cleanup
- 在类组件中我们有componentWillUnmount来清理一些副作用,类似计时器或者websocket链接
- 在函数组件中,我们可以使用useEffect的第一个参数也就是回调函数的返回值来清理副作用
1 | useEffect(() => { |
- 我们可以清理副作用
1 | useEffect(() => { |
Reducers
And now…
The joy of useReducer
由于js对于Object类型传递的为引用而不是具体值,所以如果我们只是单纯的mutate一个数组(原生API push等方法),setState无法侦测到state的变化,也就无法进行重新渲染,所以我们要使用一些immutable的方法,创建新的引用。
例如以下代码
1 | const Application = () => { |
- 当单个grudge改变时,这个列表会重新渲染,这显然不是我们想要的。
Reducer Action & State
Reducer Function
Reducer的概念本身非常简单,就是一个函数,接收两个参数,一个是当前state,一个是action,最终返回新的state。
当与React结合起来时,就是每当新的state产生,就通过重新传递props的方式来通知React重新渲染子组件
一个简单的reducer可能如下
1 | const reducer = (state, action) => { |
- 可见reducer只是一个普通函数,我们可以与useReducer结合使用,同时也因为其只是一个简单函数,我们可以方便的编写测试用例进行测试
1 | const [grudges, dispatch] = useReducer(reducer, initialState) |
Reducer Action Keys & dispatch
因为reducer的action使用字符串作为属性,推荐将字符串保存为常量,以提供更加好的代码提示与纠错
我们可以使用useReducer将上述功能修改为如下
1 | const GRUDGE_ADD = 'GRUDGE_ADD'; |
React.memo & useCallback
- 我们希望如果当前属性值与上次相同,那么就不在重新渲染依赖属性值的子组件。React提供了几种方式
- React.memo()
- useCallback
- useMemo
Context
Prop Drilling & Context API
And now… The Perils Prop Drilling
Prop drilling occurs when you have deep component trees
And now… The Context API
Creating a Context Provider
Context provides a way to pass data through the component tree without having to pass props down manually at every level
react.createContext() => provider + consumer
1 | import React from 'react' |
1 | <CountContext.Provider value={0}> |
- Example
1 | const CountContext = createContext() |
GrudgeContext
1 | import React, { createContext } from 'react'; |
Context & useContext Hook
当使用context时,优点是我们不必像之前一样到传递状态并担心状态在传递中是否正确。因此我们可以随意移动我们的UI组件并不必担心状态传递的问题。
- Some Testing Notes
- We lost all of our performance optimizations when moving to the Context API.
- What’s the right answer? It’s a trade off.
- Grudge List might seem like a toy application, but it could also represent a smaller part of a larger system.
- Could you use the Context API to get things all of the way down to this level and then use the approach we had previously?
Data Fetching
Data Fetching & useEffect Hook
And now… What about fetching data?
useEffect is your friend
useEffect第二个参数一般来说不可省略,否则组件会不停重复加载
1 | const useFetch = url => { |
Refactoring to a Custom Reducer
1 | const LOADING = "LOADING"; |
Thunks
What is a Thunk
- thunk(noun): a function returned from another function
1 | function definitelyNotAThunk() { // 顾名思义,这个函数不是thunk |
But, why is this useful?
The major idea behind a thunk is that it is code to be executed later
1 | const useThunkReducer = (reducer, initialState) => { |
1 | const fetchCharacters = dispatch => { |
Routing & Thunks
Implementing Undo & Redo
- And now… Advanced Patterns: Implementing Undo & Redo
1 | { |
1 | const useUndoReducer = (reducer, initialState) => { |
- 这里说了句俏皮话
- A week of coding save us hours of planning.
Managing State in a Form
- 当我们使useState来维护一个表单组件时,可能我们要维护多个state。我们可以编写一个useSetState来解决这个问题
1 | const reducer = (previousState = {}, updateState = {}) => { |
1 | const initialState = { |
Wrapping Up
- And now… The Ecosystem of Third-Party Hooks