<span class="text-highlight">disclaimer: this post is copy-paste heavy and is meant for my personal reference in the future</span>
References to the original authors:
useState = 1 output, 1 input useReducer = 1 output, multiple inputs (that can be grouped together to do one 'thing')
state is something that stays when you function is run at different times. it's like your computer's RAM, of your function or class's short term memory.
you use it to keep track of stuff, like count, or time_since_last_accessed, or color_theme.
imagine this.setState({}) as something that updates the state that belongs to the component in a React class. useState() is something that updates a state of a variable.
this.setState({ count: this.state.count + 1 })
// not actualy the best way to update state, because it depends
// on something outside the setState functionis similar to
const [count, setCount] = useState(0)
// .... other code...
setCount(count + 1)it is a writing style (pattern) that allows you to group all your code's state changes into one.
if you are writing this.setState() or useState() multiple times to do one action (like fetch data), it's time to group them together with useReducer().
when you are using useState() multiple times to do one 'thing'.
useEffect(
() => {
const fetchData = async () => {
setIsError(false)
setIsLoading(true)
try {
const result = await axios(url)
setData(result.data)
} catch (error) {
setIsError(true)
}
setIsLoading(false)
}
fetchData()
},
[url]
)in this case you are 'fetching data', but you need three states to account for loading UI and error. with the reducer pattern:
useEffect(
() => {
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' })
try {
const result = await axios(url)
dispatch({ type: 'FETCH_SUCCESS', payload: result.data })
} catch (error) {
dispatch({ type: 'FETCH_FAILURE' })
}
}
fetchData()
},
[url]
)you're dispatching data (telling the reducer what you need) and then the reducer does the work for you, including
what does this data fetching reducer look like?
const dataFetchReducer = (state, action) => {
switch (action.type) {
case 'FETCH_INIT':
return {
...state,
isLoading: true,
isError: false,
}
case 'FETCH_SUCCESS':
return {
...state,
isLoading: false,
isError: false,
data: action.payload,
}
case 'FETCH_FAILURE':
return {
...state,
isLoading: false,
isError: true,
}
default:
throw new Error()
}
}data won't be old if you dispatch from inside a useEffect(). It will be old if you use setSomething() in useEffect() without specifying it as a dependency:
const count = 0
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1)
}, 1000)
return () => clearInterval(id)
}, [])^ count will never update
useEffect(
() => {
const id = setInterval(() => {
setCount(count + 1)
}, 1000)
return () => clearInterval(id)
},
[count]
)^ count will be updated, but setInterval and clearInterval will be called every render
###solution 1:
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1)
}, 1000)
return () => clearInterval(id)
}, [])^ count takes its previous state, so it is correct
###solution 2:
function Counter({ step }) {
const [count, dispatch] = useReducer(reducer, 0)
function reducer(state, action) {
if (action.type === 'tick') {
return state + step
} else {
throw new Error()
}
}
useEffect(
() => {
const id = setInterval(() => {
dispatch({ type: 'tick' })
}, 1000)
return () => clearInterval(id)
},
[dispatch]
)
return <h1>{count}</h1>
}This pattern disables a few optimizations so try not to use it everywhere, but you can totally access props from a reducer if you need to.
this is lifted from Dan Abramov's very lucid blog about react:
when you dispatch, React just remembers the action — but it will call your reducer during the next render. At that point the fresh props will be in scope, and you won’t be inside an effect.