-
Notifications
You must be signed in to change notification settings - Fork 4
Lesson 08: Built in React APIs
Daniel Kraus edited this page Jul 2, 2022
·
7 revisions
Speaker: Yulia Butyrskaya
-
Pull Request - Implementation of Login Form. (Will later on be replaced with a form library)
- Pull Request Draft - Advanced: Form State Management, Step 2 - useLoginForm. (Won't be merged to the main branch)
- Pull Request Draft - Advanced: Form State Management, Step 3 - useForm. (Won't be merged to the main branch)
- Recording (Gdrive)
- Slides (Google Slides)
- Slides (PDF)
Classes
- Are required to access state and lifecycle methods
- Are confusing, comes with an annoying boilerplate, and is hard to minify and optimize
- The logic split across different lyfecycle methods makes it hard to follow it through a component
HOC & Render Props
- Are the most popular patterns to share stateful logic because React doesn't have any official way to do that
- It makes it difficult to follow data flow through the app and debug
- HOC creates "Wrapping hell"
- Painful refactoring
- Let you use state and other React features without writing a class
- Made it possible to colocate and reuse stateful logic together
- You can write your own
Don'ts ❌
- Don't call Hooks inside loops, conditions, or nested functions
- Don't call Hooks from regular JavaScript functions
Dos ✅
- Call Hooks from React function components
- Call Hooks from custom Hooks
- Lets to add state to function components
- Declares a 'state variable' that stays preserved between re-renders
const Counter = () => {
const [count, setCount] = useState(0)
return (
<div>
<button onClick={() => setCount(count + 1)}>Click</button>
<p>You clicked {count} times</p>
</div>
)
}
- We can calculate a new state based on the previous one:
setCount((previousValue) => previousValue + 1)
- We can reset state value using reserved
key
prop:
const Counter = () => {
const [count, setCount] = useState(0)
return (
<div>
<button onClick={() => setCount(count + 1)}>Click</button>
<p>You clicked {count} times</p>
</div>
)
}
const App = () => {
const [key, setKey] = useState(0)
return (
<>
<Counter key={key} />
<button onClick={() => setKey(key + 1)}>Reset</button>
</>
)
}
- If the initial value is a result of an expensive computation, we can pass a function, which will be executed only on the initial render:
const getInitialValue = () => {
// expensive computation
return 42
}
const [value, setValue] = useState(getInitialValue) // note: we're not calling this function there!
- Lets to perform side effects in function components
- In other words it runs some additional code after (re-)render
- By default, it runs both after the first render and after every update, but it is customizable:
- No dependency array => runs on every (re-)render
- Empty array
[]
=> runs on the first render only - Array with values
[value1, value2]
=> runs only when valueX has changed
const Greeting = ({ initialName = '' }) => {
console.log('%c [Child] render start', 'color: LightCoral')
const getInitialValue = () => {
console.log('%c [Child] getInitialValue', 'color: LightCoral')
return window.localStorage.getItem('name') || initialName
}
const [name, setName] = useState(getInitialValue)
const handleChange = (event) => {
setName(event.target.value)
}
useEffect(() => {
console.log('%c [Child] useEffect', 'color: LightCoral')
window.localStorage.setItem('name', name)
}, [name]) // without adding `name` to array `useEffect` will run on every re-render caused by a parent component
return (
<section>
<div>
<label htmlFor="name">Name: </label>
<input value={name} onChange={handleChange} />
{name && <p>Hello {name}!</p>}
</div>
</section>
)
}
const App = () => {
console.log('%c[App] render start', 'color: green')
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount((previousCount) => previousCount + 1)}>
Render ({count})
</button>
<Greeting initialName="George" />
</>
)
}
- Some effects (e.g., subscription) require cleanup to prevent memory leaks. If an effect returns a function, React will run it when it is time to clean up:
const Child = ({}) => {
console.log('%c [Child] render', 'color: LightCoral')
const [value, setValue] = useState(0)
useEffect(() => {
console.log('%c [Child] useEffect start', 'color: LightCoral')
const intervalId = window.setInterval(() => {
setValue((v) => v + 1)
}, [1000])
return () => {
console.log('%c Child: useEffect cleanup', 'color: LightCoral')
window.clearInterval(intervalId)
}
}, [])
return <div>Interval {value}</div>
}
const App = () => {
console.log('%c[App] render', 'color: green')
const [show, setShow] = useState(false)
return (
<>
<button onClick={() => setShow(!show)}>{show ? 'Hide' : 'Show'}</button>
{show && <Child />}
</>
)
}
- Returns a memoized value
- Used as an optimization to avoid expensive calculations on every render
- "Give the same value unless arguments have changed"
const computeExpensiveValue = (a) => {
console.log('%cComputing expensive value...', 'color: LightCoral')
return a + 42
}
const App = () => {
const [value, setValue] = useState(0)
const [count, setCount] = useState(0)
// this will be called on every re-render
// const memoizedValue = computeExpensiveValue(value)
const memoizedValue = useMemo(() => {
return computeExpensiveValue(value)
}, [value])
return (
<>
<p>Memoized value: {memoizedValue}</p>
<div>
<button onClick={() => setCount(count + 1)}>Render ({count})</button>
<button onClick={() => setValue(value + 1)}>Increase value</button>
</div>
</>
)
}
- Returns a memoized callback
- "Don't create a new instance of a function unless arguments have changed"
- Useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders
- useCallback(fn, deps) is equivalent to useMemo(() => fn, deps)
const List = ({ onItemClick }) => {
console.log('%c List: render', 'color: LightCoral')
return (
<div>
{[...Array(5)].map((_, index) => (
<div
key={index}
onClick={() => onItemClick(index)}
>
{index} item
</div>
))}
</div>
)
}
// this child component is optimized
const MemoList = memo(List)
const App = () => {
console.log('%c[App] render start', 'color: green')
const [count, setCount] = useState(0)
// this will make our optimized child component re-render
// const handleItemClick = (id) => {
// console.log(`%c[App] handleItemClick ${id}`, 'color: green')
// }
const handleItemClick = useCallback(
(id) => {
console.log(`%c[App] handleItemClick ${id}`, 'color: green')
},
[]
)
return (
<>
<button onClick={() => setCount(count + 1)}>Render ({count})</button>
<MemoList onItemClick={handleItemClick} />
</>
)
}
- Lets to reference a value that’s not needed for rendering
- It is like useState without re-rendering
- Mostly used to access DOM elements
const App = () => {
const [count, setCount] = useState(0)
const myRef = useRef(0)
return (
<>
<div>
{/* changing the current value won't cause a re-render */}
<button onClick={() => (myRef.current += 1)}>
Ref: {myRef.current}
</button>
<button onClick={() => setCount(count + 1)}>State: {count}</button>
</div>
</>
)
}
- Accepts a reducer - a pure function that accepts state and action and returns a new state
- Useful for the case when we have complex states that depend on each other
function reducer(state, action) {
if (action.type === 'loading') {
return {
...state,
loading: true,
error: undefined,
data: undefined,
}
}
if (action.type === 'success') {
return {
...state,
data: action.data,
loading: false,
error: undefined,
}
}
if (action.type === 'error') {
return {
...state,
loading: false,
error: action.error,
data: undefined,
}
}
}
const App = () => {
const [state, dispatch] = useReducer(reducer, {
loading: false,
data: undefined,
error: undefined,
})
useEffect(() => {
const fetchData = async () => {
dispatch({ type: 'loading' })
try {
const data = await apiCall()
dispatch({ type: 'success', data })
} catch (err) {
dispatch({ type: 'error', error })
}
}
fetchData()
}, [])
return null
}
- High order component
- Returns a memoized component
- Example:
List
component in useCallback
- A technique for passing a ref through a component to one of its children
- Useful when we need to pass ref to our custom React component
const MyCustomInput = ({ ref }) => {
// We can't pass ref like that
return <input ref={ref} placeholder="custom input" />
}
const MyCustomInputWithRef = forwardRef((props, ref) => {
return <input ref={ref} {...props} />
})
const App = () => {
const nativeInputRef = useRef(null)
const customInputRef = useRef(null)
const customInputForwardRef = useRef(null)
return (
<div>
<div>
<button onClick={() => nativeInputRef.current.focus()}>Focus</button>
<input ref={nativeInputRef} placeholder="native input" />
</div>
{/* this won't work */}
<div>
<button onClick={() => customInputRef.current.focus()}>Focus</button>
<MyCustomInput ref={customInputRef} placeholder="custom input" />
</div>
<div>
<button onClick={() => customInputForwardRef.current.focus()}>
Focus
</button>
<MyCustomInputWithRef
ref={customInputForwardRef}
placeholder="forwardRef"
/>
</div>
</div>
)
}
- Lets to define a component that is loaded dynamically.
- This helps reduce the bundle size to delay loading components that aren’t used during the initial render.
- Requires <React.Suspense> component higher in the rendering tree
// This component is loaded dynamicaally
const SomeComponent = React.lazy(() => import('./some-component'))
- Lets to specify the loading indicator in case some components in the tree below are not yet ready to render.
- At the moment only supports lazy loading components
- Will handle more use cases in the future
// This component is loaded dynamically
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
// Displays <Spinner> until OtherComponent loads
<React.Suspense fallback={<Spinner />}>
<div>
<OtherComponent />
</div>
</React.Suspense>
)
}