What I know about React so far Part 2
More on useEffect
The second argument controls when and how often it's called
const [time, setTime] = useState(new Date());
useEffect(() => {
// notice that we are not using
// setInterval
// because we are always rerendering
// it could be done with setInterval as well though
const timer = setTimeout(setTime(new Date()), 1000);
return () => clearTimeout(timer);
// it keeps running, because we didn't provide a
// list of dependencies, as the second parameter
});
If we had done useEffect(() => {} [])
with the []
as the second parameter, it would have only ran once.
According to the React team, you should do }, [setTime]);
, because it's a dependency, even though it's also a function and so would never change. On the other hand, if you would do }, [time]);
it would again run every 1000ms, because time
does change
useEffect cleanup
Is done by the function it returns, so in the above example it's the
return () => clearTimeout(timer);
bit.
It's a great pattern when you need to integrate external libraries in your app.
Sharing state in Reach
The simplest solution is to move the state and event handlers up from the children and pass them down as props. And also pass the event handlers down as props:
<MyComponent path="/results" handleFooChange={this.handleFooChange} />
// and later, on the child:
<button onClick={this.props.handleFooChange} />
In most situation this is enough, but for more complex scenarios Context is the advanced solution.
Note on spreading object properties
Brian Holt doesn't recommend it, as it's obscures what exactly you are passing down, and what exactly (which subset) your component needs. He doesn't recommend using <MyComponent> {...this.props} />
either based on his experience of working with React.
In fact when you do <MyComponent {...obj} />
, you are using the JSX version of the spread operator.
Context
Context is a state
alternative, that you can update anywhere in your React tree. It can replace the need for Redux. Available since React 16.
The idea is that you have a Context Provider and Consumer. Often you would create a React.createContext
and pass it an "empty" object, where the properties do exist, but their values are empty. It's mostly for testing purposes and TypeScript.
Then in your App.js
(root compoonent), you initialise the this.state
, then, you wrap your child components in <Provider value={this.state}><MyComponent /></Provider>
.
// MyContext.js
const MyContext = React.createContext({
name: '',
handleNameChange: () {}
})
// then later in
// App.js
import { Provider } from './MyContext'
class App extends React.Component {
constructor (props) {
super(props)
this.state = {
name: 'foo',
this.handleNameChange: handleNameChange
}
}
handleNameChange = event => {
this.setState({
name: event.target.value
})
}
render () {
return (
<div>
<Provider value={this.state}>
<MyComponent />
</Provider>
</div>
)
}
}
// and then in
// MyComponent
import { Consumer } from './MyComponent'
class MyComponent extends React.Component {
render() {
return (
<Consumer>
{function(context){
// and here `context` is the same
// as the state defined in App.js
return (
// your markup here
// and change
// this.state.whatever
// to
// context.whatever
)
}}
</Consumer>
)
}
}
Of course, MyComponent
can be anywhere in the React tree, as deep as you want and this has access to this context.
Accessing Context in lifecycle methods
Since <Context>
is a component and so is always in render
, you need to wrap your whole component with it, best done at the export
declaration:
export default function MyComponentWithContext(props) {
return (
<Consumer>
{context => <MyComponent {...props} mycontext={context} />}
</Consumer>
)
}
// and now you can access all Context values as props
// this.props.mycontext.name
From the V4 of the course:
Creating a context, with an indentity function for the updater:
import React, { useState, useContext, createContext } from "react";
// this is just a "fake" object
// just to let people know the shape of the object
const UserContext = createContext([
{
firstName: "Bob",
lastName: "Bobberson",
suffix: 1,
email: "bobbobberson@example.com"
},
// the updater function o
// "identity" function
// so it's the users of the context
// who will update the state (think immutable!)
// continue reading at `ContextComponent`
obj => obj
]);
// ... later you wrap your app with the context
// this is the top level component
const ContextComponent = () => {
// **** 1. create the hook. ↴
const userState = useState({
firstName: "James",
lastName: "Jameson",
suffix: 1,
email: "jamesjameson@example.com"
});
return (
// **** 2. wrapping it in the context. ↴
<UserContext.Provider value={userState}>
<h1>first level</h1>
<LevelTwo />
</UserContext.Provider>
);
};
then later, you would update it with immutable operations, because the updater function is just an identity function:
// ...
const [user, setUser] = useContext(UserContext);
// then later...
<button
// **** 5. the increment function works here too. ↴
// using the hook, and context
// which is on a grand-grand-grand parent...
onClick={() => {
setUser(Object.assign({}, user, { suffix: user.suffix + 1 }));
}}
>
Increment
</button>
useRef vs useState values
The useRef
hook was introduced in the Portals presentation. We used it to hold a reference to the "other" DOM root element that would host the modal component in the opened state.
// **** Ref.js ****
import React, { useState, useEffect, useRef } from "react";
const RefComponent = () => {
const [stateNumber, setStateNumber] = useState(0);
const numRef = useRef(0);
// **** 1. here the stateNumber and numRef. ↴
// won't match
// they are incremented at the same time,
// but they are not the same
// stateNumber "lags" behind
function incrementAndDelayLogging() {
// what's going on, is that this `stateNumber` below, is
// is holding onto the previous iteration
// because of closure
setStateNumber(stateNumber + 1);
// while numRef is always holding onto to the
// CURRENT value
numRef.current++;
setTimeout(
() => alert(`state: ${stateNumber} | ref: ${numRef.current}`),
1000
);
}
return (
<div>
<h1>useRef Example</h1>
<button onClick={incrementAndDelayLogging}>delay logging</button>
<h4>state: {stateNumber}</h4>
<h4>ref: {numRef.current}</h4>
</div>
);
};
export default RefComponent;
So what happens is that calling setStateNumber
does update the stateNumber
, but in the function that called it (incrementAndDelayLogging
), the stateNumber
is still pointing to the value before the setStateNumber
call. While the numRef.current
is directly pointing to the latest value.
This presents a great case for useRef
. In your render function, the stateNumber
value will always be the correct one, but elsewhere, as the above example demonstrated, it's better to use useRef
.
It's useful to hold on to DOM elements, intervals, etc.
Notice, that you can only use .current
here, no other property, the object is sealed.
Reducers (Redux replacement)
useReducer
combined with Context can replace entirely the need for Redux.
import React, { useReducer } from "react";
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return Object.assign({}, state, {counter: state.counter++})
case 'decrement':
return Object.assign({}, state, {counter: state.counter--})
default:
return saate
}
}
const ReducerComponent = () => {
const [{counter}, dispatch] = useReducer(reducer, {counter})
}
return (
<div>
<button onClick={() => dispatch({ type: 'increment' })}>➕</button>
<button onClick={() => dispatch({ type: 'decrement' })}>➖</button>
</div>
)
Again, combining useReducer
with Context is a great way to manage the global state of your application and replace Redux.
Performance optimizations
useMemo to optimise render time calculations
You can use useMemo
to memoise expensive calculations that would happen inside your render
function.
Works similar to other memoisation function form other libraries.
import React, { useState, useMemo } from "react";
const fibonacci = n => {
if (n <= 1) {
return 1;
}
return fibonacci(n - 1) + fibonacci(n - 2);
};
const MemoComponent = () => {
const [num, setNum] = useState(1);
// ...
const fib = useMemo(() => fibonacci(num), [num]);
return (
<div>
Fibonacci of {num} is {fib}
<button onClick={() => setNum(num + 1)}>➕</button>
</div>
)
}
useCallback to partially render your component
You can combine useCallback
and memo
to memoise the rendering of a part of your component.
import React, { useState, useEffect, useCallback, memo } from "react";
const ExpensiveComputationComponent = memo(({ compute, count }) => {
return (
<div>
<h1>computed: {compute(count)}</h1>
<h4>last re-render {new Date().toLocaleTimeString()}</h4>
</div>
);
});
// then later...
return (
<div>
<ExpensiveComputationComponent
// **** 4. use the useCallback. ↴
// same as useMemo
// give it a function and the list of dependencies
compute={useCallback(fibonacci, [])}
count={count}
/>
</div>
)
don't overuse useCallback
and useMemo
!
Only add them, when you start to have a problem. If you add them too early, then you can quickly make a huge mess...
useLayoutEffect
It's useful when you need to measure things in the DOM. (getBoundClientRect
, etc).
import React, { useState, useLayoutEffect, useRef } from "react";
const LayoutEffectComponent = () => {
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);
// **** 2. storing the measurements here. ↴
const el = useRef();
// **** 1. we're measuring the elements here. ↴
// **** 4. it actually kicks off another rerender. ↴
// it calls the LayoutEffectComponent twice,
// but actually only paints once
useLayoutEffect(() => {
setWidth(el.current.clientWidth);
setHeight(el.current.clientHeight);
});
// useLayoutEffect works just like any other Effect
// you could do
// }, [])
// to add dependencies
return (
<div>
<h1>useLayoutEffect Example</h1>
<h2>textarea width: {width}px</h2>
<h2>textarea height: {height}px</h2>
<textarea
onClick={() => {
// **** 3. we're setting unintuitively to 0 . ↴
// has no meaning,
// but it kicks off the rerender
setWidth(0);
}}
// this will store the actual DOM element,
// passing it to useLayoutEffect
// console.log(el)
// would show the actual textarea element
ref={el}
/>
</div>
);
};
export default LayoutEffectComponent;
The difference between useEffect and useLayoutEffect
useEffect
is (technically) asynchronous, it happens sometime after the rendering, while useLayoutEffect
is more synchronous, as it's triggered right after the rendering has taken place.
(notice the word technically, the difference is still very small)
Redux
If you have a lot of asynchronous code in your app, for example making dozens of API calls. Even Dan Abramov the creator Redux, says that he doesn't use them much anymore.
The steps of a Redux circle,
1 User types in input box 2 Call action creator to get an action 3 Dispatch action to Redux 4 Redux inserts the action into the root reducer 5 The root reducer delegates that action to the correct reducer 6 The reducer returns a new state given the old state and the action object 7 That new state becomes the store's state 8 React is then called by Redux and told to update
Redux is not React specific, you can use it with other frameworks such as Angular or Ember, or even without a framework.
Creating a Redux store
Import createStore
and initialise it with a reducer.
// **** store.js ****
import { createStore } from "redux";
import reducer from "./reducers";
const store = createStore(
reducer,
// this is just for using
// React devtools
// if React devtools exist, use them
// otherwise just give a "bogus" function (`f => f`)
typeof window === "object" &&
typeof window.__REDUX_DEVTOOLS_EXTENSION__ !== "undefined"
? window.__REDUX_DEVTOOLS_EXTENSION__()
: f => f
);
export default store;
Creating the reducers
This is just some more boilerplate.
// **** reducers/index.js ****
import { combineReducers } from "redux";
import location from "./location";
export default combineReducers({
location: location
});
Then finally one reducer
// **** reducers/location.js ****
// adding a default value for state...
export default function location(state = "Seattle, WA", action) {
switch (action.type) {
case "CHANGE_LOCATION":
return action.payload;
default:
return state;
}
}
the action object
The action
object above is provided by redux.
You should always return a state
this is why switch with a default
case is great.
The payload
property
While the action
property is a hard requirement, payload
is part of the flux standard action.
It's available as a GitHub repo. It's a description of the action shapes.
Creating and dispatching actions
You can dispatch them directly, or use actionCreators
that return the action definitions.
// **** actionCreators/changeLocation.js ****
export default function changeLocation(location) {
return { type: "CHANGE_LOCATION", payload: location };
}
Connecting Redux to your app
First, you will no longer be using useState
hooks, but using the store
we created above.
import { Provider } from "react-redux";
import store from './store'
const App = () => {
<Provider store={store}>
// .. app code
</Provider>
}
Then in one of your component
import { connect } from "react-redux";
// read the props...
const mapStateToProps = ({ theme }) => ({ theme });
// connect it ↴
const WrappedDetails = connect(mapStateToProps)(Details);
return (
<WrappedDetails {...props} />
)