react hooks

useState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// basic
const [name, setName] = useState('')
setName('')

// set state depends on prevState
const [count, setCount] = useState(0)
setCount(prevCount => prevCount + 1)

// state is object
const [name, setName] = useState({
firstName: '',
lastName: ''
})
setName({
...name, firstName: 'sam'
})

// state is array
const [items, setItems] = useState([])
setItems({
...items,
{
id: items.length,
value: Math.random()
}
})

useEffect

Create side effects replacement for componentDidMount, componentDidUpdate, componentWillUnMount

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// basic
const [count, setCount] = useState(0)
useEffect(() => {
document.title = `Count is ${count}`
})

// run effect only when props or state changes
useEffect(() => {
document.title = `Count is ${count}`
}, [count])

// run effect only once, just like componentDidMount
useEffect(() => {
console.log('only run once')
window.addEventListener('mousemove', logMousePosition)
}, [])

// cleanup when unmount, like componentWillUnMount
useEffect(() => {
console.log('only run once')
window.addEventListener('mousemove', logMousePosition)
return () => {
console.log('run when unmount')
window.removeEventListener('mousemove', logMousePosition)
}
}, [])

setInterval

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// depends on count
const tick = () => {
setCount(count + 1)
}
useEffect(() => {
const interval = setInterval(tick, 1000)
return () => {
clearInterval(interval)
}
}, [count])

// depends on prevCount
const tick = () => {
setCount(prevCount => prevCount + 1)
}
useEffect(() => {
const interval = setInterval(tick, 1000)
return () => {
clearInterval(interval)
}
}, [])

fetching data only once

1
2
3
4
5
6
7
8
9
10
11
12
const [data, setData] = useState([])
useEffect(() => {
axios
.get('https://localhost/api')
.then(res => {
console.log(res)
setData(res.data)
})
.catch(err => {
console.log(err)
})
}, [])

fetching data depends on state change

1
2
3
4
5
6
7
8
9
10
11
12
const [id, setId] = useState(1)
useEffect(() => {
axios
.get(`https://localhost/api/posts/${id}`)
.then(res => {
console.log(res)
setData(res.data)
})
.catch(err => {
console.log(err)
})
}, [id])

useContext

Context provides a way to pass data through the component three without having to pass props down manually at every level.

Basic usage:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// App.js
export const UserContext = React.createContext()
export const ChannelContext = React.createContext()
function App(){
return (
<div className='App'>
<UserContext.Provider value={'Michael Jackson'}>
<ChannelContext.Provider value={'channel value'}>
<ComponentC />
</ChannelContext.Provider>
</UserContext.Provider>
</div>
)
}

// use Consumer ComponentF inside ComponentD inside ComponentC
import { UserContext, ChannelContext } from '../App'
function ComponentF(){
return <div>
<UserContext.Consumer>
{
user => {
return (
<ChannelContext.Consumer>
{
channel => {
return <div>User context value {user}, channel context value {channel}</div>
}
}
</ChannelContext.Consumer>
}
}
</UserContext.Consumer>
</div>
}

// use useContext to get value directly
import { UserContext, ChannelContext } from '../App'
function ComponentE(){
const user = useContext(UserContext)
const channel = useContext(ChannelContext)
return (
<div>{user} - {channel}</div>
)
}

useReducer

useReducer is a hook that is used for state management in React.

reduce in Javascript useReducer in React
array.reduce(reducer, initialState) useReducer(reducer, initialValue)
singleValue = reducer(accumulator, itemValue) newState = reducer(currentState, action)
reduce method returns a single value useReducer returns a pair of values, [newState, dispatch]

Imitate redux reducer, but a local state management:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// CounterOne.js
const initialState = {
count: 1
}
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return { count: state.count + action.value }
case 'decrement':
return { count: state.count - action.value }
case 'reset':
return initialState
default:
return state
}
}

function CounterOne() {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<div>
<div>Count - {state.count}</div>
<button onClick={() => dispatch({ type: 'increment', value: 2 })}>
Increment
</button>
<button onClick={() => dispatch({ type: 'decrement', value: 2 })}>
Decrement
</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
)
}

useReducer with useContext as a global state management:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// App.js
export const CountContext = React.createContext()

const initialState = {
count: 1
}
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return { count: state.count + action.value }
case 'decrement':
return { count: state.count - action.value }
case 'reset':
return initialState
default:
return state
}
}
function App() {
const [countState, countDispatch] = useReducer(reducer, initialState)
return (
<CountContext.Provider value={{ countState, countDispatch }}>
Count - {countState.count}
<ComponentC />
</CountContext.Provider>
)
}

// ComponentF.js
import { CountContext } from '../App'

function ComponentF() {
const countContext = useContext(CountContext)
return (
<div>
<div>Count - {countContext.countState.count}</div>
<button
onClick={() =>
countContext.countDispatch({ type: 'increment', value: 2 })
}
>
Increment
</button>
<button
onClick={() =>
countContext.countDispatch({ type: 'decrement', value: 2 })
}
>
Decrement
</button>
<button onClick={() => countContext.countDispatch({ type: 'reset' })}>
Reset
</button>
</div>
)
}

Fetching data with useReducer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const initialState = {
loading: true,
error: '',
post: {}
}
const reducer = (state, action) => {
switch (action.type) {
case 'FETCH_SUCCESS':
return {
loading: false,
post: action.payload,
error: ''
}
case 'FETCH_ERROR':
return {
loading: false,
post: action.payload,
error: 'something went wrong'
}
default:
return state
}
}
function DateFetching() {
const [state, dispatch] = useReducer(reducer, initialState)
useEffect(() => {
axios.get('http://fsa/url').then(res => {
dispatch({ type: 'FETCH_SUCCESS', payload: res.data }).catch(error => {
dispatch({ type: 'FETCH_ERROR' })
})
})
}, [])
return (
<div>
{state.loading ? 'loading' : state.post.title}
{state.error ? state.error : ''}
</div>
)
}

useState vs useReducer

Scenario useState useReducer
Type of state Number,String,Boolean Object or Array
Number of state transitions One or two Too many
Related state transitions? No Yes
Business logic No business logic Complex business logic
Local vs Global Local Global

useCallback

For performance optimization, we have to restrict re-renders to only components that need to re-render.

useCallback is a hook that will return a memorized version of the callback function that only changes if one of the dependencies has changed.

It is useful when passing callbacks to optimized child components that reply on reference equality to prevent unnecessary renders.

React.memo可以使子组件只有接受的 props 改变时才会渲染,防止父组件 re-render 导致全部子组件 re-render。

对于一般的非引用类型变量React.memo可以起作用,但是由于 function 每次定义地址并不相同,还是会 re-render,所以需要useCallback创建一个根据条件变化的 function。

useCallback是为了防止 re-render 后由于重新定义了 function,导致以 function 作为 props 传入的子组件也进行了不必要的渲染。

example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Button.js
function Button({ onClick, children }) {
console.log('render button', children)
return <button onClick={onClick}>{children}</button>
}
export default React.memo(Button)

// Count.js
function Count({ text, count }) {
console.log('render count', text)
return (
<div>
{text} - {count}
</div>
)
}
export default React.memo(Count)

// ParentComponent.js
function ParentComponent() {
const [age, setAge] = useState(1)
// incrementAge only change when age change
const incrementAge = useCallback(() => {
setAge(age + 1)
}, [age])

const [salary, setSalary] = useState(1)
// incrementSalary only change when age change
const incrementSalary = useCallback(() => {
setSalary(salary + 1)
}, [salary])

return (
<div>
<Count text='age' count={age} />
<Button onClick={incrementAge}>incrementAge</Button>
<Count text='salary' count={salary} />
<Button onClick={incrementSalary}>incrementSalary</Button>
</div>
)
}

useMemo

useMemo is a hook that will only recompute the cached value when one of the dependencies has changed, this optimisation heads to avoid expensive calcuations on every render.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function UseMemoTest(){
const [one, setOne] = useState(0)
const [two, setTwo] = useState(0)
const incrementOne = () => {
setOne(one + 1)
}
const incrementTwo = () => {
setTwo(two + 1)
}
// only execute when one changed
const isEven = useMemo(() => {
let i = 0
while(i < 20000000) i++
return one % === 0
}, [one])
return (
<div>
<button onClick={incrementOne}>count one {one}</button>
<span>{isEven ? 'even': 'odd'}</span>
<button onClick={incrementTwo}>count two {two}</button>
</div>
)
}

useRef

useRef is used to access dom node.

1
2
3
4
5
6
7
8
9
10
11
12
function FocusInput() {
const inputRef = useRef(null)
useEffect(() => {
// focus input
inputRef.current.focus()
}, [])
return (
<div>
<input ref={inputRef} type='text' />
</div>
)
}

useRef can be used to store data, and not cause re-render when data change, because only current change, the object that useRef return is persistent.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function HookTimer() {
const [timer, setTImer] = useState(0)
const intervalRef = useRef()

useEffect(() => {
intervalRef.current = setInterval(() => {
setTimer(prevTimer => prevTimer + 1)
}, 1000)
return () => {
clearInterval(intervalRef.current)
}
}, [])
return (
<div>
timer - {timer}
<button onClick={() => clearInterval(intervalRef.current)}>
clear interval
</button>
</div>
)
}

React.forwardRef

It can be useful for some kinds of components, especially in reusable component libraries.

1
2
3
4
5
6
7
8
9
10
11
const Button = React.forwardRef((props, ref) => (
<button ref={ref} className='custom-button'>
{props.children}
</button>
))

// usage
function App() {
const ref = React.createRef()
return <Button ref={ref}>click me</Button>
}

useImperativeHandle

1
useImperativeHandle(ref, createHandle, [deps])

should be used with forwardRef.

ref change, handle function run.

1
2
3
4
5
6
7
8
9
10
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

useLayoutEffect

The signature is identical to useEffect, but it fires synchronously after all DOM mutations. Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.

Prefer the standard useEffect when possible to avoid blocking visual updates.

useDebugValue

useDebugValue can be used to display a label for custom hooks in React DevTools.

1
2
3
4
5
6
7
8
9
10
11
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null)

// ...

// Show a label in DevTools next to this Hook // e.g. "FriendStatus: Online" useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline
}

// In some cases formatting a value for display might be an expensive operation. It’s also unnecessary unless a Hook is actually inspected.
useDebugValue(date, date => date.toDateString())

Custom hooks

share useEffect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// useDocumentTitle.js
function useDocumentTitle(count) {
useEffect(() => {
document.title = `Count ${count}`
}, [count])
}
export default useDocumentTitle

// app.js
import useDocumentTitle from './hooks/useDocumentTitle'
function App() {
const [count, setCount] = useState(0)
useDocumentTitle(count)
return (
<div>
<button onClick={() => setCount(count + 1)}>count - {count}</button>
</div>
)
}

share useState and methods

1
2
3
4
5
6
7
8
9
10
11
12
13
function useCounter(initialCount = 0) {
const [count, setCount] = useState(initialCount)
const increment = () => {
setCount(prevCount => prevCount + 1)
}
const decrement = () => {
setCount(prevCount => prevCount - 1)
}
const reset = () => {
setCount(initialCount)
}
return [count, increment, decrement, reset]
}

share input change logic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// useInput.js
function useInput(initialValue) {
const [value, setValue] = useState(initialValue)
const reset = () => {
setValue(initialValue)
}
const bind = {
value: value,
onChange: e => {
setValue(e.target.value)
}
}
return [value, bind, reset]
}
export default useInput

// App.js
import useInput from './hooks/useInput'
function App() {
const [firstName, bindFirstName, resetFirstName] = useInput('')
return (
<form>
<input {...bindFirstName} />
</form>
)
}

react-redux hooks

You can use react-redux hooks to replace connect function in FC.

useSelector

1
2
3
4
function Loading() {
const loading = useSelector((state: AppState) => state.users.loading)
return loading && <CircularProgress />
}

useDispatch

1
2
3
4
5
6
7
function Login(){
const dispatch = useDispatch()
const login = useCallback(data => dispatch(loginAction(data)), [dispatch])
return <div>
<button onClick={() => login({username, password})}>
</div>
}

useStore

1
2
3
4
5
6
7
export const CounterComponent = ({ value }) => {
const store = useStore()

// EXAMPLE ONLY! Do not do this in a real app.
// The component will not automatically update if the store state changes
return <div>{store.getState()}</div>
}

Reference:

https://reactjs.org/docs/hooks-reference.html

https://www.youtube.com/playlist?list=PLC3y8-rFHvwisvxhZ135pogtX7_Oe3Q3A

https://react-redux.js.org/next/api/hooks