React hooks
History
-
React.createClass
- Pass with an object with information to describe the component
- JS didn't have a built in class system
-
React.Component
- React v0.13.0
- Allowed creating components from (now) native JS classes
- Initialize the state of the component inside the constructor
- If you're extending a subclass need to invoke
super
before usingthis
- Also have to remember to pass props
- Also have to
.bind
methods
createClass
automatically bound all the methods to the component
- Class Fields
- Add instance properties directly on a class without using
constructor
- Can use arrow functions (note: instantiated for every object)
- Add instance properties directly on a class without using
- Duplicate logic
- Have to sprinkle related logic throughout the component
- Need 3 separate methods to keep state in sync
componentDidMount
- Initial fetchcomponentDidUpdate
- Re fetch on state change- update method - Trigger the state change
- Sharing non-visual logic
- Have another component that needs the same kind of state
- Could duplicate code - nah
- Create a Higher order component with the logic and pass as props
- Can get unwieldy
-
export default withHover(withTheme(withAuth(withRepos(Profile))))
- Component tree now has a bunch of wrappers which makes it harder to follow
-
React hooks
- Use a class if component has state, or needs lifecycle methods
- Can use a function if just props and UI rendering
- What if we could just use a function
- State
-
useState
- Is a hook
- Takes a single argument - initial value for the state
- Returns array with the first item being the sate and the second being a function to update the state
const [loading, setLoading] = React.useState(true);
-
- Lifecycle methods
- Think about synchronization instead of lifecycle
-
React.useEffect
- Allows performing side effects in function components
- Arguments: function, optional array
- function: side effects to run
- array: when to re-run the effect
- by default run after first render and every update
- Instead array of state values will only trigger when that value changes (as well as mount, unmount)
- Should include all values from the component scope that change and are used by the effect
- Otherwise it will reference stale values from previous renders
- Empty array will on mount and unmount
- Sharing non-visual logic
- Create your own hook
- Function
- Passed state objects
- Synchronizes with
useEffect
- Returns state objects
function useRepos(id) { const [repos, setRepos] = React.useState([]); const [loading, setLoading] = React.useState(true); React.useEffect(() => { setLoading(true); fetchRepos(id).then((repos) => { setRepos(repos); setLoading(false); }); }, [id]); return [loading, repos]; }
function ReposGrid ({ id }) { const [ loading, repos ] = useRepos(id) }
Managing state
-
useState
- Allows triggering a component re-render
- Preserve values between renders
- Unless you are using closures expect all variables in function to be GCd
- React preserves state values between calls to the function to render
- Each unique piece of state has its own useState invocation (and value, update function)
setState
would merge new object with previous state (to just set a couple of values)useState
replaces, values should be separate- If the most logical data type for a piece of state is an object better to use
useReducer
- When updating state based on previous pass a function to the updater function, similar to
setState
- Lazy initialization
function Counter() { const [count, setCount] = React.useState(getExpensiveCount()); ... }
getExpensiveCount
called for every Counter invocation, even though the result is only used once for the initial value- pass
useState
a function it will run the first time on the initial render and ignore later
function Counter() { const [count, setCount] = React.useState(() => getExpensiveCount()); ... }
Adding side effects
-
useEffect
- side effect: State change that can be observed outside it's local environment
- Add an effect
React.useEffect(() => { document.title = "The new title."; });
- Invoked after every render by default
- After React has updated DOM - side effect won't block the UI update
- Skipping side effects
- Second argument
- Array of state values
- Will invoke whenever one of those values changes, instead of every render
- Cleaning up side effect
- If you return a function from the effect it will be invoked before the component is unmounted
- If component is re-rendered the cleanup function will be invoked before the new render
import { subscribe, unsubscribe } from './api' function Profile ({ username }) { const [profile, setProfile] = React.useState(null) React.useEffect(() => { subscribe(username, setProfile) return () => { unsubscribe(username) setProfile(null) } }, [username]) if (profile === null) { return <p>Loading...</p> } return ( <React.Fragment> <h1>@{profile.login}</h1> <img src={profile.avatar_url} alt={`Avatar for ${profile.login}`} /> <p>{profile.bio}</p> </React.Fragment> ); }
- Cleanup will be invoked in two cases
- When username changes
- Before Profile is unmounted
- NOTE: Cleanup is also done after render, but before re-running effect
Rules of hooks
- Can only be called from the top-level of a function component or a custom hook
- Not from function, class component, loop, if statement,event handler
- React relies on the call order of Hooks to keep track of internal state and references
Creating custom hooks
- Both higher order components and render props force you to restructure the tree and lead to heavy nesting
- Decouple the non visual logic from the UI with a hook
- create a function whose name start with
use
function useHover () { const [hovering, setHovering] = React.useState(false) const mouseOver = () => setHovering(true) const mouseOut = () => setHovering(false) const attrs = { onMouseOver: mouseOver, onMouseOut: mouseOut } return [hovering, attrs] }
function Info ({ id, size }) { const [hovering, attrs] = useHover() return ( <React.Fragment> {hovering === true ? <Tooltip id={id} /> : null} <svg {...attrs} width={size} viewBox="0 0 16 16" > ... </svg> </React.Fragment> ) }
Managing complex state
let state = 0;
nums.forEach((v) => state+=v);
- forEach is dependent on state of application, also modifies state outside it's scope - impure
- Best to avoid impure functions
- Use
reduce
- Keeps track of state internally, accumulates into it with passed in function
const total = nums.reduce((state, value)=>(
state+value
), 0);
-
useReducer
- Adds state to a function component, but managed using the reducer pattern
- Need a way for user actions to invoke our reducer function
- Returns an array with the first element being the
state
and second being adispatch
which invokes the reducer - What you pass to dispatch will be passed as the second argument to the
reducer
function reducer(state, action) { if (action === "increment") { return state + 1; } else if (action === "decrement") { return state - 1; } else if (action === "reset") { return 0; } else { throw new Error(`This action type isn't supported.`); } } function Counter() { const [count, dispatch] = React.useReducer(reducer, 0); return ( <React.Fragment> <h1>{count}</h1> <button onClick={() => dispatch("increment")}>+</button> <button onClick={() => dispatch("decrement")}>-</button> <button onClick={() => dispatch("reset")}>Reset</button> </React.Fragment> ); }
- Decoupled update logic of count state from the component
- Mapping actions to state transitions - separate how state updates from the action
- Changing by a variable amount
function reducer(state, action) { if (action.type === "increment") { return { count: state.count + state.step, step: state.step, }; } else if (action.type === "decrement") { return { count: state.count - state.step, step: state.step, }; } else if (action.type === "reset") { return { count: 0, step: state.step, }; } else if (action.type === "updateStep") { return { count: state.count, step: action.step, }; } else { throw new Error(`This action type isn't supported.`); } } function Counter() { const [count, dispatch] = React.useReducer(reducer, 0); return ( <React.Fragment> <Slider onChange={(value) => dispatch({ type: "updateStep", step: value, }) } /> <hr /> <h1>{state.count}</h1> <button onClick={() => dispatch({ type: "increment", }) } > + </button> <button onClick={() => dispatch({ type: "decrement", }) } > - </button> <button onClick={() => dispatch({ type: "reset", }) } > Reset </button> </React.Fragment> ); }
- Reducer better for updating state that depends on other state
- Declarative state updates
- Registration form with username, email, password
- UX state: loading, error, registered
function Register() { const [username, setUsername] = React.useState(""); const [email, setEmail] = React.useState(""); const [password, setPassword] = React.useState(""); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(""); const [registered, setRegistered] = React.useState(false); const handleSubmit = (e) => { e.preventDefault(); setLoading(true); setError(""); newUser({ username, email, password }) .then(() => { setLoading(false); setError(""); setRegistered(true); }) .catch((error) => { setLoading(false); setError(error); }); }; if (registered === true) { return <Redirect to="/dashboard" />; } if (loading === true) { return <Loading />; } return ( <React.Fragment> {error && <p>{error}</p>} <form onSubmit={handleSubmit}> <input type="text" placeholder="email" onChange={(e) => setEmail(e.target.value)} value={email} /> <input type="text" placeholder="username" onChange={(e) => setUsername(e.target.value)} value={username} /> <input placeholder="password" onChange={(e) => setPassword(e.target.value)} value={password} type="password" /> <button type="submit">Submit</button> </form> </React.Fragment> ); }
- Declarative approach - describe what we want to accomplish instead of how
- Reducer encapsulates imperative, instructional code
function registerReducer(state, action) { if (action.type === "login") { return { ...state, loading: true, error: "", }; } else if (action.type === "success") { return { ...state, loading: false, error: "", registered: true, }; } else if (action.type === "error") { return { ...state, loading: false, error: action.error, }; } else if (action.type === "input") { return { ...state, [action.name]: action.value, }; } else { throw new Error(`This action type isn't supported.`); } } const initialState = { username: "", email: "", password: "", loading: false, error: "", registered: false, }; function Register() { const [state, dispatch] = React.useReducer( registerReducer, initialState, ); const handleSubmit = (e) => { e.preventDefault(); dispatch({ type: "login" }); newUser({ username: state.username, email: state.email, password: state.password, }) .then(() => dispatch({ type: "success" })) .catch((error) => dispatch({ type: "error", error, }), ); }; if (state.registered === true) { return <Redirect to="/dashboard" />; } if (state.loading === true) { return <Loading />; } return ( <React.Fragment> {state.error && <p>{state.error}</p>} <form onSubmit={handleSubmit}> <input type="text" placeholder="email" onChange={(e) => dispatch({ type: "input", name: "email", value: e.target.value, }) } value={state.email} /> <input type="text" placeholder="username" onChange={(e) => dispatch({ type: "input", name: "username", value: e.target.value, }) } value={state.username} /> <input placeholder="password" onChange={(e) => dispatch({ type: "input", name: "password", value: e.target.value, }) } value={state.password} type="password" /> <button type="submit">Submit</button> </form> </React.Fragment> );
- Minimizing
useEffect
dependency array- Leave it off - infinite loop of it always updating
- Forget to add values - stale data
- State values are encapsulated inside reducer, don't need to add to array
React.useEffect(() => { const id = window.setInterval(() => { setCount(count + 1); }, 1000); return () => window.clearInterval(id); }, [count]);
- Every time count changes a new interval is created
React.useEffect(() => { const id = window.setInterval(() => { dispatch({ type: "increment" }); }, 1000); return () => window.clearInterval(id); }, []);
- Can do this example without reducer
React.useEffect(() => { const id = window.setInterval(() => { setCount((count) => count + 1) }, 1000) return () => window.clearInterval(id) }, [])
- Count doesn't need to be in the array because the value comes from setCount instead of the function component
- But if we add a step variable to add instead of 1 adds a dependency to the array
setCount
passes the old variable of count to be updated- Need to get the up to date value of step - rerun effect when value changes
- Putting step, count in a reducer means it would be provided by the setState function
New render, same value
useRef
hook- Not dealing with ui - don't need to re-render
- But want to persist value
- Don't want to call set state since it will re-render
function usePersistentValue(initialValue) { return React.useState({ current: initialValue, })[0]; }
useRef
does this- Gives object with current property that persists across renders
const nameRef = React.useRef();
<input placeholder="name" type="text" ref={nameRef} />
- When passing a ref to the ref prop on a DOM element react will set the ref's current property to the corresponding DOM element
- Persisting a handle across renders
function Counter() { const [count, setCount] = React.useState(0); let id = React.useRef(null); const clear = () => { window.clearInterval(id.current); }; React.useEffect(() => { id.current = window.setInterval(() => { setCount((c) => c + 1); }, 1000); return clear; }, []); return ( <div> <h1>{count}</h1> <button onClick={clear}>Stop</button> </div> ); }
React contexts
- See (react)[./react]
-
useMemo
- Makes sure the value it returns stays the same unless variable from the list changes
export default function App () { const [locale, setLocale] = React.useState('en') const toggleLocale = () => { setLocale((locale) => { return locale === 'en' ? 'es' : 'en' }) } const value = React.useMemo(() => ({ locale, toggleLocale }), [locale]) return ( <LocaleContext.Provider value={value}> <Home /> </LocaleContext.Provider> ) }
- Constructing a new function each invocation, but it doesn't trigger a re-render because
useMemo
is hiding the change
- Makes sure the value it returns stays the same unless variable from the list changes
-
useContext
- Render-props syntax gets messy, especially with nested consumers
export default function Nav() { return ( <AuthedContext.Consumer> {({ authed }) => authed === false ? ( <Redirect to="/login" /> ) : ( <LocaleContext.Consumer> {({ locale, toggleLocale }) => locale === "en" ? ( <EnglishNav toggleLocale={toggleLocale} /> ) : ( <SpanishNav toggleLocale={toggleLocale} /> ) } </LocaleContext.Consumer> ) } </AuthedContext.Consumer> ); }
useContext
takes a context object and returns what was passed to the value prop of the nearest provider- More composable API
export default function Nav() { const { authed } = React.useContext(AuthedContext); const { locale, toggleLocale } = React.useContext(LocaleContext); if (authed === false) { return <Redirect to="/login" />; } return locale === "en" ? ( <EnglishNav toggleLocale={toggleLocale} /> ) : ( <SpanishNav toggleLocale={toggleLocale} /> ); }
- Render-props syntax gets messy, especially with nested consumers
Performance
- Same use intuition as functions for components
- Instead of function taking arguments and returning value, component takes props and returns ui (jsx)
- Hooks allow components to just be functions
- Like a function any code int he component will be executed when it re-renders
React.memo
HOC that lets you skip re-rendering a component if it's props haven't changed- When components is wrapped in
React.memo()
react will render the component and memoize the result - On re-render React will do a shallow comparison of the old and new props, if they match will skip re-render
- Primitive vs reference values
- React.memo uses the identity function to compare props (==)
- When passing in a new object or function it will have a new reference and not match the previous value
- Can pass a second argument to
React.memo
with a custom comparison, or useuseCallback
-
useCallback
- Returns a memoised callback
- Function won't be re-created every render
- Second param is a dependency list which will trigger re-creating the function
- NOTE:
NthFib
andNthPrime
have been exported wrapped inReact.memo
function App() { const [fibCount, setFibCount] = React.useState(1) const [primeCount, setPrimeCount] = React.useState(1) const handleReset = () => { setFibCount(1) setPrimeCount(1) } const add10 = () => { setFibCount((c) => c + 10) setPrimeCount((c) => c + 10) } const incrementFib = React.useCallback(() => setFibCount((c) => c + 1), [] ) const incrementPrime = React.useCallback(() => setPrimeCount((c) => c + 1), [] ) return ( <React.Fragment> <button onClick={add10}>Add 10</button> <button onClick={handleReset}>Reset</button> <hr /> <NthFib count={fibCount} increment={incrementFib} /> <hr /> <NthPrime count={primeCount} increment={incrementPrime} /> </React.Fragment> ); }
-
useMemo
- Could
useMemo
the result of the computation instead of the whole sub-component - Don't use
useMemo
to persist values- React treats it as a performance hint instead of a guarantee, it might forget the memoised value in certain situations
- Could