diff --git a/Programming_Languages/React/Hooks/Application_state_management.md b/Programming_Languages/React/Hooks/Application_state_management.md new file mode 100644 index 0000000..d9f8ef6 --- /dev/null +++ b/Programming_Languages/React/Hooks/Application_state_management.md @@ -0,0 +1,88 @@ +--- +tags: + - Programming_Languages + - javascript + - react + - react-hooks +--- + +# Application state management + +Although [useReducer](./useReducer.md) and [useContext](./useContext.md) have many sound use cases by themselves, when they are combined they offer a way to acheive a system of global state management, without utilising third-party libraries like Redux. + +## Requirements + +The Context API combined with a reducer addresses the following needs: + +- be able to consume global state from anywhere in the component hierarchy without prop-drilling +- update global state from within child components from anywhere in the component hierarchy +- access, maintain and update complex structured data (arrays/objects) not just single values, as global state + +## Implementation + +In essence the approach is as follows: + +- Create a context provider that houses the global state object +- Attach this state to a reducer function that operates on the state object through dispatch methods + +First off, our initial state and overall state object: + +```js +const initialState = { + count: 0, +}; +``` + +Next, we create our context. This is what we will invoke when we want to consume our state. + +```js +export const CounterContext = React.createContext({}); +``` + +Now we need a reducer that will handle the state updates. We will just use the same setup as we used in the example of [useReducer](./useReducer.md#refining-the-syntax): + +```js +function reducer(state, action) { + switch (action.type) { + case 'increase': + return {...state, counter: state.counter + 1}; + break; + case 'decrease': + return {...state, counter: state.counter - 1}; + break; + case 'increase_by_payload': + return {...state, counter: state.counter + action.payload}; + default: + throw new Error(); + } + return newState; +} +``` + +Finally, we need to make a provider as a top-level component that receives the reducer's `state` and `dispatch` methods as props: + +```js +export const CounterProvider = ({children}) => { + // We pass our reducer function and initial state to useReducer as params + const [state, dispatch] = React.useReducer(reducer, initialState) + + // We create our provider and pass the reducer state and update method as props. This is the provider to the CounterContext consumer + + {children} + +} +``` + +Now whenever we want to consume or update our global state, we can invoke the context within a lower-level component, for example: + +```js +const {state, dispatch} = useContext(CounterContext); + +dispatch({ + type 'increase_by_payload', + payload: 4 +}) + +console.log(state.counter) // 4 + +``` diff --git a/Programming_Languages/React/Hooks/Memoization.md b/Programming_Languages/React/Hooks/Memoization.md new file mode 100644 index 0000000..c677664 --- /dev/null +++ b/Programming_Languages/React/Hooks/Memoization.md @@ -0,0 +1,107 @@ +--- +tags: + - Programming_Languages + - javascript + - react + - react-hooks + - memoization +--- + +# Memoization with `useCallback` and `useMemo` + +## Rationale + +In the lifecycle of a component, React re-renders the component when an update is made. When React checks for any changes in a component, it may detect an unintended or unexpected change due to how JavaScript handles equality and shallow comparisons. This change in the React application will cause it to re-render unnecessarily. + +Additionally, if that re-rendering is an expensive operation, like a long for loop, it can hurt performance. Expensive operations can be costly in either time, memory, or processing. In addition to potential technical issues, this may lead to poor user experience. + +If one part re-renders, it re-renders the entire component tree. `useCallback` and `useMemo` can be used reduce this performance impact. + +## useCallback + +The `useCallback` hook is used to wrap functions. It tells React to not re-create a wrapped function when a component re-renders, unless any of the useCallback's dependencies change. + +`useCallback` returns a memoized version of the callback function it is passed. This means that the function object returned from useCallback will be the same between re-renders. + +Remember that in JavaScript, functions are objects and components are functions. As a result, every time a component containing a function re-renders, it creates a new instance of the function in memory. + +> Given the same dependency value, the `useCallback` hook returns the same function instance between renderings (aka memoization). + +This said, for small functions that are not intensive, it doesn't really matter if they are not memoized. + +### Syntax + +A standard case of this would be a function that runs on a button click, for instance when sending data from a form to a server. In the example below there is quite a lot going on, and most of it is independent of the actual UI-cycle of the component. + +```jsx +const handleSubmit = useCallback( + async (formValues) => { + setPendSaveConfig(true); + const payload = new GenerateConfig({ + workflowId: project_id, + blockId: blockId, + config: formValues, + }); + axios + .post(`${process.env.REACT_APP_ENDPOINT}/save-block-config`, payload) + .then((res) => console.log(res)) + .finally(() => setPendSaveConfig(false)) + .catch((err) => console.error(err)); + }, + [blockId, project_id], +); +``` + +Note that the syntax is similar to [useEffect](./useEffect.md): there is a dependency array. The effect is the same: the function contained within `useCallback` will only re-rendered if one of these dependencies changes. However (see next section) the function will run in its memoized form on every click. + +### Reference versus result + +`useCallback` only memoizes the function _object_ (the reference) not the value that is _returned_ by the function (the result). + +In the example below, the `calculatePi()` function reference will not change between renders but each time the click event fires, the value returned by `calculatePi()` will change. In other words, it will be assigned fresh memory. + +```jsx +function ParentComponent() { + const onHandleClick = useCallback(() => { + const special = calculatePi(); + }); + + return ; +} +``` + +### Use cases + +You should not apply `useCallback` in a blanket fashion, this can reduce performance rather than improve it. The best scenarios are: + +1. A functional component wrapped inside React.memo() accepts a function object prop + +2. When the function object is a dependency to other hooks, e.g. `useEffect(..., [callback])` + +3. When the function has some internal state, e.g. when the function is debounced or throttled. + +## useMemo + +We can think of `useMemo` as a complement to `useCallback`. The main difference is that whilst `useCallback` memoizes the function reference, `useMemo` memoizes the function result; the value that is returned. + +> In memoization, the result is “remembered” when the same parameters are passed-in subsequently. If we have a function compute 1 + 1, it will return 2. But if it uses memoization, the next time we run 1’s through the function, it won’t add them up; it will just remember the answer is 2 without executing the adding function. + +Like `useCallback`, `useMemo` takes a function and an array of dependencies for the same reason: if one of the dependencies changes then the function will recalculate (and re-memoize) the result but they don't the same memoized value will be returned. + +As with `useCallback`, `useMemo` is best applied to complex functions that are computationally expensive, it shouldn't be used indiscriminately. + +It should not be used with asynchronous functions, in these instances, `useEffect` would be a better choice. + +### Syntax + +```jsx +const List = React.useMemo( + () => + listOfItems.map((item) => ({ + ...item, + itemProp1: expensiveFunction(props.first), + itemProp2: anotherPriceyFunction(props.second), + })), + [listOfItems], +); +``` diff --git a/Programming_Languages/React/Hooks/useCallback.md b/Programming_Languages/React/Hooks/useCallback.md deleted file mode 100644 index 5a6441d..0000000 --- a/Programming_Languages/React/Hooks/useCallback.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -tags: - - Programming_Languages - - javascript - - react - - react-hooks ---- - -# `useCallback` - -In short, React's useCallback hook is used to wrap functions. It tells React to not re-create a wrapped function when a component re-renders, unless any of the useCallback's dependencies change. - -`useCallback` returns a memoized version of the callback function it is passed. This means that the function object returned from useCallback will be the same between re-renders. - -Remember that in JavaScript, functions are objects. As a result, every time a component containing a function re-renders, it creates a new instance of the function in memory. If a function doesn't change in response to a UI event throughout the lifecycle of the component, there is no need for it to rerender, hence `useCallback` should be applied. - -> Given the same dependency value, the `useCallback` hook returns the same function instance between renderings (aka memoization). - -This said, for small functions that are not intensive, it doesn't really matter if they are not memoized. - -## Syntax - -A standard case of this would be a function that runs on a button click, for instance when sending data from a form to a server. In the example below there is quite a lot going on, and most of it is independent of the actual UI-cycle of the component. - -```jsx -const handleSubmit = useCallback( - async (formValues) => { - setPendSaveConfig(true); - const payload = new GenerateConfig({ - workflowId: project_id, - blockId: blockId, - config: formValues, - }); - axios - .post(`${process.env.REACT_APP_ENDPOINT}/save-block-config`, payload) - .then((res) => console.log(res)) - .finally(() => setPendSaveConfig(false)) - .catch((err) => console.error(err)); - }, - [blockId, project_id], -); -``` - -Note that the syntax is similar to [useEffect](./useEffect.md): there is a dependency array. The effect is the same: the function contained within `useCallback` will only re-rendered if one of these dependencies changes. However (see next section) the function will run in its memoized form on every click. - -## Reference versus result - -`useCallback` only memoizes the function _object_ (the reference) not the value that is _returned_ by the function (the result). - -In the example below, the `calculatePi()` function reference will not change between renders but each time the click event fires, the value returned by `calculatePi()` will change. In other words, it will be assigned fresh memory. - -```jsx -function ParentComponent() { - const onHandleClick = useCallback(() => { - const special = calculatePi(); - }); - - return ; -} -``` - -## Use cases - -You should not apply `useCallback` in a blanket fashion, this can reduce performance rather than improve it. The best scenarios are: - -1. A functional component wrapped inside React.memo() accepts a function object prop - -2. When the function object is a dependency to other hooks, e.g. `useEffect(..., [callback])` - -3. When the function has some internal state, e.g. when the function is debounced or throttled. diff --git a/Programming_Languages/React/Hooks/useContext.md b/Programming_Languages/React/Hooks/useContext.md new file mode 100644 index 0000000..a952216 --- /dev/null +++ b/Programming_Languages/React/Hooks/useContext.md @@ -0,0 +1,107 @@ +--- +tags: + - Programming_Languages + - javascript + - react + - react-hooks +--- + +# `useContext` + +We use React's Context API to enable us to easily share stateful data between all levels of an application without having to use repetitious 'prop-drilling' at a component level. + +This works best with data that is global by definition, such as theme parameters or access authorisation. Context should not be used when standard and proximate compositional methods and prop-passing are sufficient. + +## Providers and consumers + +The Context API preceded the introduction of hooks in React 16.8 and it still underpins context management in hooks, although the syntax is simplified. + +The process is as follows: + +- Initiate a context +- Create a parent `Provider` component that owns the data that will be passed down +- Create child `Consumer` components that receive the data of the parent. + +We will demonstrate using a set of styles as the context that we want to pass around our app. + +```jsx +const style = { + border: '2px solid dodgerblue', + background: 'lightblue', + color: 'dodgerblue', +}; +``` + +Le's initiate a context: + +```jsx +const StyleContext = React.createContext(); +``` + +Now that we have our data and have initialized a context, we can apply it to our app components: + +```jsx +... +``` + +This is the parent element of the context environment. This stores the contextual data as a prop. Next we need to make a component that acts as the consumer of this data. Let's create this component and call it `Child` for simplicity: + +```jsx +const Child = () => { + return ( + + {(value) => ( +
+

+ I'm a child. I consume of the data my parent provides. +

+
+ )} +
+ ); +}; +``` + +The contents of this component are wrapped in tags that reference the specific context (`StyleContext`) and the role that the component plays: `Consumer`. + +We are taking the `value` prop owned by `StyleContext.Provider` and passing it to the wrapping component of `Child` as a `style` tag. This means that `Child` will display these styles. + +The final step is just to insert our `` components beneath the parent component, e.g.: + +```jsx + + + + +``` + +## `useContext` hook + +The hook removes the need to explicitly declare `Provider` and `Consumer` components; their role becomes more implicit. + +To demonstrate let's use a different example. We are going to have a single integer as the data context, `42`. + +First invoke and intialize the Context API, just as before: + +```jsx +const NumberContext = React.createContext(42); +``` + +Next, we just need to create our provider component. We don't have to worry about a consumer since this is handled implicitly by the invocation of the hook + +```jsx +const Context = () => { + const data = useContext(NumberContext); + return {data}; +}; +``` + +Then, in our code we just insert the `Context` component: + +```jsx +... +``` + +## Updating context state + +In the examples above we have only been consuming state that is owned by the provider however in most scenarios you will also want to update the state from a consumer. This is best achieved by combining `useContext` with a reducer and is detailed in [Application state management](./Application_state_management.md). diff --git a/Programming_Languages/React/Hooks/useMemo.md b/Programming_Languages/React/Hooks/useMemo.md deleted file mode 100644 index e69de29..0000000 diff --git a/Programming_Languages/React/Hooks/useReducer.md b/Programming_Languages/React/Hooks/useReducer.md new file mode 100644 index 0000000..17effa7 --- /dev/null +++ b/Programming_Languages/React/Hooks/useReducer.md @@ -0,0 +1,120 @@ +--- +tags: + - Programming_Languages + - javascript + - react + - react-hooks +--- + +# `useReducer` + +The `useReducer` hook is best used in scenarios where you are manipulating state in a way that is too complex for the trivial [useState](useState.md) use case. `useState` is best employed when you are updating a single value or toggling a boolean. If you are updating the state of an object or more complex data structure, it is often more efficient to employ `useReducer`. + +This makes the code more manageable and also helps with separating state management from rendering. + +## Syntax + +```jsx +const [state, dispatch] = useReducer(reducer, initialState); +``` + +- `initialState` + - The starting state, typically an object +- `reducer` + - A pure function that accepts two parameters: + - The current state + - An action object + - The reducer function must update the current state (immutably) and return the new state + - We can think of the reducer as working in the same manner as `state`/`setState` in the `useState` hook. The functional role is the same, it is just that the reducer offers more than one type of update. + +### Example reducer + +```js +function reducer(state, action) { + let newState; + switch (action.type) { + case 'increase': + newState = {counter: state.counter + 1}; + break; + case 'descrease': + newState = {counter: state.counter - 1}; + break; + default: + throw new Error(); + } + return newState; +} +``` + +In this example we are updating an object with the following shape: + +```js +{ + counter: 0, +} +``` + +This would be the `initialState` that we pass to the `useReducer` hook along with a reference to `reducer` above. + +To update the state we would invoke the `dispatch` function which applies one of the actions defined in the reducer. For example the following dispatch increments the counter by one: + +```js +dispatch({type: 'increase'}); +``` + +To view the updated value: + +```js +console.log(state.counter); +``` + +### Refining the syntax + +Because React doesn't mutate state, the reducer doesn't directly modify the current state in the `state` variable, it creates a new instance of the state object on each update. + +In the reducer example above this is achieved by declaring a variable `newState` that is updated by each `action` type and then returned. There is a more elegant way of doing this using spread syntax: + +```js +function reducer(state, action) { + switch (action.type) { + case 'increase': + return {...state, counter: state.counter + 1}; + break; + case 'decrease': + return {...state, counter: state.counter - 1}; + break; + default: + throw new Error(); + } + return newState; +} +``` + +### Including payloads + +In the examples so far, we have updated the the state directly via the action type however it is also possible to pass data along with the `action.type` as `action.payload`. + +For example: + +```js +dispatch( + { + type: 'increase_by_payload' + payload: 3, + }); +``` + +Then we would update our reducer to handle this case: + +```js +function reducer(state, action) { + switch (action.type) { + ... + case 'increase_by_payload': + return {...state, counter: state.counter + action.payload} + default: + throw new Error(); + } + return newState; +} +```