- Lifecycle Methods
- Unexpected Side Effects
- React 16.3: How to handle side effects
- Parent & Child Communication
- Component vs. PureComponent
- Portals
- HOCs
- Provider
- Suspense
There are many reasons, but one is that interacting with the DOM is one the most expensive operations you can do in Javascript in terms of performance. We should use it only when we have to, and this is one of the key principles that SPA frameworks try to take care of for us. For example, React will calculate exactly which nodes in the DOM need to be re-rendered and only re-render those.
As of React 16.3, some commonly used lifecycle methods have been deemed UNSAFE
by the React team and will be completely deprecated by React 17. The problem with some of the lifecycle methods is that they were often misused or misunderstood, which led to cases where side effects (state/prop changes) were not behaving as expected.
First, we need to understand the work that React does to update our UI:
- The
render
phase. This determines what changes need to be made in the DOM (forreact-dom
at least). React will callrender
and compare the result to the previous render. - The
commit
phase. This is when React actually applies our changes. In the case ofreact-dom
, this is when React inserts, updates and removes DOM nodes. During this phase, lifecycle methods likecomponentDidMount
andcomponentDidUpdate
will be called.
Note: For async rendering (coming soon in React 16.x), the rendering work will be paused and resumed to avoid blocking the browser. The main reason for this is that the commit phase is usually very fast, while rendering can be slow. React may invoke the render phase several times before actually committing the work (i.e. updating the UI).
During the render
phase, React can call any of the following lifecycle methods:
constructor
componentWillMount
componentWillReceiveProps
componentWillUpdate
getDerivedStateFromProps
shouldComponentUpdate
render
setState
The above methods can and most likely will be called more than once during the render phase, so it's important that they do not contain side effects. Ignoring this can lead to memory leaks and invalid application states. It can often mean that the rendered state of the app is non-deterministic (we don't know what the output will be).
Initializing state
class ExampleComponent extends React.Component {
state = {
currentColor: this.props.defaultColor,
}
}
Fetching external data
class ExampleComponent extends React.Component {
state = { data: null }
componentDidMount() {
this.request = fetchData()
.then((data) => {
this.request = null
this.setState({ data })
})
}
componentWillUnmount() {
if (this.request) this.request.cancel()
}
render() {
if (this.state.data === null) {
// Render loading state
} else {
// Render UI with data
}
}
}
Previously, some people would do this kind of thing using componentWillMount
. If the data is not immediately available when componentWillMount
fires, the first render
will still show a loading state regardless of where you initiate your fetch.
Updating state based on props
class ExampleComponent extends React.Component {
state = {
scrollingDown: false,
lastRow: null,
}
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.currentRow !== prevState.lastRow) {
return {
scrollingDown: nextProps.currentRow > prevState.lastRow,
lastRow: nextProps.currentRow,
}
}
return null
}
}
What's happening here is not too important, but the point is that getDerivedStateFromProps
should now be used where componentWillReceiveProps
was often used before.
Performing side-effects according to prop changes
class ExampleComponent extends React.Component {
componentDidUpdate(prevProps, prevState) {
if (this.props.isVisible !== prevProps.isVisible) {
logVisibleChange(this.props.isVisible);
}
}
}
Before 16.3, we would often invoke side effects in componentWillReceiveProps
. This method will often get called multiple times during a single update, so it's important to ensure it's only called once per update.
Reading DOM properties before an update
class ScrollingList extends React.Component {
listRef = null;
getSnapshotBeforeUpdate(prevProps, prevState) {
// Are we adding new items to the list?
// Capture the current height of the list so we can adjust scroll later.
if (prevProps.list.length < this.props.list.length) {
return this.listRef.scrollHeight;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// If we have a snapshot value, we've just added new items.
// Adjust scroll so these new items don't push the old ones out of view.
// (snapshot here is the value returned from getSnapshotBeforeUpdate)
if (snapshot !== null) {
this.listRef.scrollTop +=
this.listRef.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={this.setListRef}>
{/* ...contents... */}
</div>
);
}
setListRef = ref => {
this.listRef = ref;
};
}
Again, what's happening here is not too important, but it's important to see how getSnapshotBeforeUpdate
works. This method will get called immediately before mutations are made to the DOM and componentDidUpdate
is called immediately after mutations are made.
In any frontend framework, this topic always comes up and questions get asked about it all the time. In React there are several ways you can achieve communication between parent and child components, so here are just a few:
class TheChild extends Component {
childMethod() {
return 'hello'
}
}
class TheParent extends Component {
render() {
return <TheChild ref={(foo) => { this.foo = foo }} />
}
componentDidMount() {
const x = this.foo.childMethod()
// x is now 'hello' thanks to TheChild's `childMethod`
}
}
When rendering the child, we attach a ref
(reference) with a name of our choosing so we can refer to the child throughout our parent component.
We can also pass a function from the parent to the child as props. The child can use this function to communicate with its parent. The Parent could also render several children, which can all have the ability to update a value within the parent.
const TheChild = ({ parentFunc }) => <div>{props.parentFunc()}</div>
class TheParent extends Component {
constructor(props) {
super(props)
this.parentFunc = this.parentFunc.bind(this)
}
parentFunc() {
return 'Hello from TheParent'
}
render() {
return <TheChild parentFunc={this.parentFunc} />
}
}
What is PureComponent
? The only difference between it and Component
is that PureComponent
will also handle the shouldComponentUpdate
method for us. PureComponent
performs a shallow comparison on both props
and state
to decide if the component should update or not. For a Component
, it will re-render by default any time props
or state
changes.
When comparing props
and state
, it will check that primitives have the same values and that references are the same for Arrays and Objects. This behaviour encourages immutability when it comes to Arrays and Objects, and always return new references rather than mutating the existing value.
Let's say you have an Array being passed into a PureComponent
, and you want to add a new item to the Array. If you just wrote myArray.push(1)
, the PureComponent
would see a reference to the same Array, and not update. However, if we used a function that returned an entirely new Array with the new item added to it, our PureComponent
would indeed update.
class Parent extends Component {
likeComment(userID) {
// ...
}
render() {
return (
// ...
<Comment likeComment={() => this.likeComment(user.id)} />
// ...
)
}
}
Consider this simple Parent
which renders a Comment
. If Parent
re-renders because of a change to another prop or state value, the entire Comment
component will also re-render. Every time Parent
re-renders, it creates an entirely new function and passes it in as the likeComment
prop. If we had a list of comments, you can see how this could negatively impact performance.
class Parent extends Component {
likeComment = (userID) => {
// ...
}
render() {
return (
// ...
<Comment likeComment={this.likeComment} userID={user.id} />
// ...
)
}
}
class Comment extends PureComponent {
// ...
handleLike() {
this.props.likeComment(this.props.userID)
}
// ...
}
The same logic also applies when doing something like this:
<Comments comments={this.props.comments || []} />
What we are expecting is that if there are no comments passed into the component, it will send an empty Array to the component. What actually happens here is that a new Array with a new reference is going to be used every time the render()
method is ran. Instead, we can create a constant outside of our component (e.g. const defaultComments = []
) and reference that.
For the same reason as above, we also shouldn't create new Arrays or Objects directly inside the render()
method.
render() {
const { posts } = this.props
const topTen = posts.sort((a, b) => b.likes - a.likes).slice(0, 9)
return //...
}
Here we're creating a new Array called topTen
when the component renders. topTen
will have a brand new reference each time the component re-renders, even if posts
hasn’t changed. This will then re-render the component unnecessarily.
There are a couple of ways we could solve this. The first would be to only set topTen
when we know the value of posts
has changed:
componentWillMount() {
this.setTopTenPosts(this.props.posts)
}
componentWillReceiveProps(nextProps) {
if (this.props.posts !== nextProps.posts) {
this.setTopTenPosts(nextProps)
}
}
setTopTenPosts(posts) {
this.setState({
topTen: posts.sort((a, b) => b.likes - a.likes).slice(0, 9)
})
}
Another approach is to use recompose
to create a new prop
called topTen
which is derived from the posts
prop:
export default compose(
withPropsOnChange(['posts'], ({ posts }) => {
return {
topTen: posts.sort((a, b) => b.likes - a.likes).slice(0, 9)
}
})
)(Component)
And finally, you could consider using reselect
to create selectors which return the derived data from Redux.
Portals were a added in React 16, and straight away libraries such as react-modal used Portals to improve their libraries. Portals allow us to create links between a component and an element. In the case of creating a modal, most people that have used a modal library will know how annoying styling, controlling state, sharing state and HTML markup can be. The idea for Portals is that you can create one <div>
outside of your React <div>
and simply create a Portal to that <div>
from inside your app.
Let's take a look at an example:
class ExternalPortal extends Component {
constructor(props) {
super(props);
// STEP 1
this.containerEl = document.createElement('div');
this.externalWindow = null;
}
componentDidMount() {
// STEP 3
this.externalWindow = window.open('', '', 'width=600,height=400,left=200,top=200');
// STEP 4
this.externalWindow.document.body.appendChild(this.containerEl);
// STEP 5
this.externalWindow.addEventListener('beforeunload', () => {
this.props.handleClosePortal();
});
}
componentWillUnmount() {
this.externalWindow.close();
}
render() {
// STEP 2
return ReactDOM.createPortal(this.props.children, this.containerEl);
}
}
What's happening here?
- Create an empty
<div>
- Create a Portal with
this.props.children
as its children, and render those inside our new empty<div>
- Upon mounting, create a new external window
- We now have a
<div>
with a Portal attached to it, which is rendering whatever children are passed into it. Here, we just append the body of our new external window with our<div>
Portal - Lastly, we listen for whenever this external window is closed. When it's closed, we'll call
this.props.handleClosePortal
which should be passed in by its parent and trigger some side-effects in the parent
So, now we have a Portal that when used, will render whatever its children are and call a parent prop when closed. Most importantly, it's rendered somewhere completely outside of the <div>
which holds our React app!
class App extends Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
showPortal: false,
};
}
componentDidMount = () => {
window.addEventListener('beforeunload', () => this.closePortal());
window.setInterval(() => {
this.setState(state => ({
counter: state.counter + 1,
}));
}, 1000);
}
togglePortal = () => this.setState({ showPortal: !this.state.showPortal })
closePortal = () => this.setState({ showPortal: false })
render() {
return (
<div>
<h1>Counter: {this.state.counter}</h1>
<button onClick={this.togglePortal}>`${this.state.showPortal ? 'Close' : 'Open'} Portal`</button>
{/* Use our Portal instance and pass in `this.state.counter` */}
{this.state.showPortal && (
<ExternalPortal handleClosePortal={this.closePortal} >
<h1>`Counter in a Portal: ${this.state.counter}`</h1>
<button onClick={() => this.closePortal()}>Close!</button>
</ExternalPortal>
)}
</div>
);
}
}
Now we have a straight forward App
component, which has some local state to store showPortal
, counter
and some methods to toggle/update those state properties. When showPortal
is true
, we render our ExternalPortal
and pass in children which take advantage of App
's local state, and also pass in App
's method to close the Portal. Magic! ✨
A higher-order component (HOC) is a function that takes a component and returns a new component. Typically, HOCs are helpers or utilities which you apply to a component to give it some additional functionality. Redux's connect()
is a good example of a HOC, which is invoked by calling connect(...)(MyComponent)
and will help connect your component to Redux.
Here's a really simple example, where I have a <User />
component which takes in a name
and renders it. Below that, I have 2 more cases that I want to achieve. With alwaysBob
, I want to be able to create a <User />
where the name
prop is always set to "Bob"
. With neverUpdate
, I want to be able to create a <User />
which never re-renders, even when receiving new props.
const User = ({ name }) => <div className="User">{ name }</div>
const User2 = alwaysBob(User)
const User3 = neverUpdate(User)
const App = () =>
<div>
<User name="Tim" />
<User2 name="Joe" />
<User3 name="Steve" />
</div>
alwaysBob
will take in a prop and simply return the BaseComponent
with {...overrideProps}
after {...props}
to override the values.
const overrideProps = (overrideProps) => (BaseComponent) => (props) =>
<BaseComponent {...props} {...overrideProps} />
const alwaysBob = overrideProps({ name: 'Bob' })
neverUpdate
will return a class
implementation of the BaseComponent
with shouldComponentUpdate
set to false to force it never to update
const neverUpdate = (BaseComponent) =>
class extends Component {
shouldComponentUpdate() {
return false
}
render() {
return <BaseComponent {...this.props} />
}
}
Easy!
We can replace the neverUpdate
HOC above with a much simpler implementation thanks to recompose
.
import { lifecycle } from 'recompose'
const neverUpdate = compose(
lifecycle({
shouldComponentUpdate() {
return false
}
})
)
const UserWithNeverUpdate = neverUpdate(User)
In this example, I'm going to add a state and some methods to the component. Normally, you'd have to go back and change the component to a Class component, then add some methods, then take care of binding the methods, then keep track of updating the state etc. Long story short, your component will look less and less like a simple UI component. Recompose, can help to add class-like methods to our functional components.
Take this Card:
const Card = ({ opened, handleClick, title, picture, description }) => {
return (
<div className={ opened ? 'card open' : 'card closed' }>
<div className="header" onClick={handleClick}>
{title}
</div>
<div className="body">
<img src={picture} />
<p>{description}</p>
</div>
</div>
)
}
It applies a dynamic class to the <div>
container depending on if this.props.opened
is true
or false
. How is it updated? The Card's header has an onClick
event attached to it which will call this.props.handleClick
. Enter recompose
again:
const enhance = compose(
withState('opened', 'setOpened', true),
withHandlers({
handleClick: props => event => {
// setOpened is applied in withState()
props.setOpened(!props.opened)
},
})
)
export default enhance(Card)
withState
is called with stateName, stateUpdaterName, initialState
and will essentially pass the first two as props
to the component and the third as defaultProps
. withHandlers
is just a nice way of adding a method to your component's props
just like you would with a class method. There are a couple of benefits from doing this with recompose
however. Recompose will take care of making sure a new handler is not created with every render. If you were to define your handler just before your return(...)
, it would create a new instance of that function on every render, which in a large application could become very costly. It could also be ignored by optimzations such as shouldComponentUpdate()
which tries to prevent re-rendering from happening.
Let's say we have a list:
const App = ({ zombies }) => {
return (
<div>
{
zombies.map(zombie => (
<Card
key={zombie.title}
title={zombie.title}
picture={zombie.picture}
description={zombie.description}
/>
))
}
</div>
)
}
I want to render a <Spinner />
if the data is still being loaded, and now that I think about it I also want to fetch the data when the App is mounted.
const enhance = compose(
lifecycle({
componentDidMount() {
this.props.fetchZombies()
}
}),
branch(
({ zombies, ...others }) => zombies.length === 0,
renderComponent(Spinner)
)
)
export default enhance(App)
As easy as that. branch()
allows us to make sure our functional component remains a really simple UI component, and moves the dynamic rendering logic into a separate function. As we saw before, we can also use lifecycle()
to access the lifecycle methods of a component without having to turn it into a class component.
Let's say you have a very simple <Date />
component which simply renders a UI for a given date.
const Date = ({ date }) => <h1>`My DOB is ${date}`</h1>
As simple as you can get, it takes in a date
and renders it. But what happens if we decide we want to format the date
? We can pass in the date
in the necessary format to <Date />
, or we can simple format the date
locally. This is the better approach as we may not want to change date
's data structure; we just want to change how the UI renders the data.
const getFormattedDate = d => `${d.getDate()}/${d.getMonth() + 1}/${d.getFullYear()}`
export default compose(
withPropsOnChange(['dob'], ({ dob }) => {
return { dob: getFormattedDate(dob) }
})
)(Date)
Now, Recompose will take care of creating this formatted prop whenever the dob
prop changes.
Background:
Diving straight in, here's a HOC which gives a component props.mouse
with x
and y
co-ordinates.
const withMouse = (Component) => {
return class extends React.Component {
state = { x: 0, y: 0 }
handleMouseMove = (event) => {
this.setState({
x: event.clientX,
y: event.clientY
})
}
render() {
return (
<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
<Component {...this.props} mouse={this.state}/>
</div>
)
}
}
}
const App = React.createClass({
render() {
// Instead of maintaining our own state,
// we get the mouse position as a prop!
const { x, y } = this.props.mouse
return (
<div style={{ height: '100%' }}>
<h1>The mouse position is ({x}, {y})</h1>
</div>
)
}
})
// Just wrap your component in withMouse and
// it'll get the mouse prop!
const AppWithMouse = withMouse(App)
ReactDOM.render(<AppWithMouse/>, document.getElementById('app'))
Now we can wrap any component and it'll receive this.props.mouse
. So what's wrong with it?
- Indirection: With multiple HOCs being used together, we can very easily be left wondering where our state comes from and wondering which HOC provides which props.
- Naming collisions: Two HOCs that try to use the same prop name will collide and overwrite one another, and it's actually quite annoying because React won’t warn us about the prop name collision either.
- Static composition: HOCs have to be used outside of React's lifecycle methods, as we see with
AppWithMouse
above, which means we can't do much in the way of making our HOCs dynamic.
Seeing the light of render props
A render prop is a function prop that a component uses to know what to render
So what does this mean? Well, the idea is this: instead of “mixing in” or decorating a component to share behaviour, just render a regular component with a render
prop that it can use to share some state with you. Here's our withMouse
HOC as a render prop:
class Mouse extends Component {
static propTypes = {
render: PropTypes.func.isRequired
}
state = { x: 0, y: 0 }
handleMouseMove = (event) => {
this.setState({
x: event.clientX,
y: event.clientY
})
}
render() {
return (
<div onMouseMove={this.handleMouseMove}>
{this.props.render(this.state)}
</div>
)
}
}
const App = () => (
<div style={{ height: '100%' }}>
<Mouse render={({ x, y }) => (
<h1>The mouse position is ({x}, {y})</h1>
)}/>
</div>
)
Now hold on, our Mouse
component is just calling this.props.render(this.state)
? Mouse
is now a component that just exposes its internal state to a render
prop. This means that App
can render whatever it wants with that state. Does this solve any of the issues we had with HOCs?
- Indirection: Yes. We no longer wonder where our state is coming from as we can see them in the render prop's arguments, and we can see it comes from
<Mouse />
- Naming collisions: Yes. We are not merging any property names together anymore, and neither is React.
- Static composition: Yes. Everything is happening inside the
render
method, so we get to take advantage of the React lifecycle.
We've already looked at render
props, so now let's look at the similar concept of functions as child components. Here's a simple example:
const MyComponent = ({ children }) => <div>{children('Declan')}</div>
// Usage
<MyComponent>
{(name) => <div>{name}</div>}
</MyComponent>
<MyComponent>
{(name) => <img src="/my-picture.jpg" alt={name} />}
</MyComponent>
As you can see, it's really easy to re-use these components as MyComponent
is just exposing some data to whatever its children
function renders.
A more complicated example
Next, we'll create a <Ratio>
component that listens for resize events and gives the device dimensions to its child.
The boilerplate:
class Ratio extends Component {
render() {
return (
{this.props.children()}
)
}
}
Ratio.propTypes = {
children: React.PropTypes.func.isRequired,
}
Now we have a simple component, and we explicitly tell the user of it that we're expecting a function as props.children
. Now let's add some internal state to Ratio
:
class Ratio extends React.Component {
constructor() {
super(...arguments)
this.state = {
hasComputed: false,
width: 0,
height: 0,
}
}
getComputedDimensions({ x, y }) {
const { width } = this.container.getBoundingClientRect()
return {
width,
height: width * (y / x),
}
}
componentWillReceiveProps(next) {
this.setState(this.getComputedDimensions(next))
}
componentDidMount() {
this.setState({
...this.getComputedDimensions(this.props),
hasComputed: true,
})
window.addEventListener('resize', this.handleResize, false)
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize, false)
}
handleResize= () => {
this.setState({
hasComputed: false,
}, () => {
this.setState({
hasComputed: true,
...this.getComputedDimensions(this.props),
})
})
}
render() {
return (
<div ref={(ref) => this.container = ref}>
{this.props.children(this.state.width, this.state.height, this.state.hasComputed)}
</div>
);
}
}
Ratio.propTypes = {
x: React.PropTypes.number.isRequired,
y: React.PropTypes.number.isRequired,
children: React.PropTypes.func.isRequired,
}
Ratio.defaultProps = {
x: 3,
y: 4
};
We did a lot here. We basically have an eventListener
listening for resize
events, and whenever it receives an event it just calculates the ratio based on the device width. After, we pass all that into our children
function. Now, we can use it however we want:
<Ratio>
{(width, height, hasComputed) => (
hasComputed
? <img src="/my-image.png" width={width} height={height} />
: null
)}
</Ratio>
<Ratio>
{(width, height, hasComputed) => (
<div style={{ width, height }}>Hello world!</div>
)}
</Ratio>
<Ratio>
{(width, height, hasComputed) => (
hasComputed && height > TOO_TALL
? <TallThing />
: <NotSoTallThing />
)}
</Ratio>
Positives:
- The user can choose what they do with the properties passed into
children
- The user can choose what property names to use as it's just a function. A lot of HOCs will force you to use their property names
- No naming collisions as there isn't any binding connection between the HOC and its child. Instead you could use 2 HOCs together that both calculate the device width and have no issues at all
- HOCs remove constants! See below for an example:
MyComponent.MyConstant = 'HELLO'
export default connect(..., MyComponent)
Normally, a HOC would remove your MyConstant
unless your HOC manually re-implements it.
This time, I'm going to have a simple <List />
component which takes in a URL and renders a list. Why is this useful? Well, let's see:
class List extends React.Component {
state = {
list: [],
isLoading: false,
}
fetchData = async () => {
const res = await fetch(this.props.url)
const json = await res.json()
this.setState({
list: json,
isLoading: false,
})
}
componentDidMount() {
this.setState({ isLoading: true }, this.fetchData);
}
render() {
return this.props.render(this.state);
}
}
Now we have a component which takes care of fetching the data and applying any internal states such as loading
. It's important to minimize duplicated code in a project to promote consistency and simplicity within the codebase (and UI). It also reduces the number of points of origin for an error when debugging, which is always good.
Here's how we would use that <List />
component:
<List
url="https://api.github.com/users/declanelcocks/repos"
render={({ list, isLoading }) => (
<div>
<h2>My repos</h2>
{isLoading && <h2>Loading...</h2>}
<ul>
{list.length > 0 && list.map(repo => (
<li key={repo.id}>
{repo.full_name}
</li>
))}
</ul>
</div>
)} />
If we know that every component will have a consistent Loading...
message, we could also extend our List
to take care of this too.
Use React, and you're almost guaranteed to run into some form of <Provider>
component. Redux, MobX or any theming library all use this pattern in order to pass down props to every component.
First, we need to understand what React's Context is, as that's what all of these Providers are using. React often encourages people not to use context, but it's a powerful feature which can be great for making all components aware of things such as state or themes. You can also use context on a much smaller scale than the entire app. Consider the following:
--A--B
\
C--D--E
Imagine that component B and E both relied on a variable or method used in component A. With no state management library such as Redux, we'd have to pass that variable down through all of the components until we got to E. With context we could just provide this variable in context, and then consume context in the components that need it. It's often said not to do this as you can very easily make a lot of components aware of things they don't need to be.
A much better example would be if you have a global theme, as every component would need to be aware of this theme in order to perform its own styling.
How?
class A extends Component {
getChildContext() {
return {
theme: "green"
};
}
render() {
return <D />;
}
}
class E extends Component {
render() {
return (
<div style={{ color: this.context.theme }}>
{this.children}
</div>
);
}
}
Here's a full example of what we mentioned above to show how context is used.
Now that we know how context works, let's use it to provide our app with a theme.
class ThemeProvider extends Component {
getChildContext() {
return { color: this.props.color }
}
render() {
return <div>{this.props.children}</div>
}
}
ThemeProvider.childContextTypes = {
color: PropTypes.string
}
// Our main file somewhere
ReactDOM.render(
<ThemeProvider color="green">
<App />
</ThemeProvider>,
document.getElementById('app')
)
class Paragraph extends React.Component {
render() {
const { color } = this.context
return <p style={{ color: color }}>{this.props.children}</p>
}
}
Paragraph.contextTypes = {
color: PropTypes.string
}
Now we have a ThemeProvider
that takes in a single prop, and sets that prop as context throughout the whole app. If you've used any state management libraries this should all be looking very familiar. In any child component of <App />
we can now access this.context.color
just as you would with any this.props
or this.state
. You may now know what's next, how do we turn this into a more friendly HOC than using this.context
everywhere?
const getContext = contextTypes => Component => {
class GetContext extends React.Component {
render() {
return <Component { ...this.props } { ...this.context } />
}
}
GetContext.contextTypes = contextTypes
return GetContext
}
This will work exactly the same as any other library. The main purpose of this is to try and reduce the amount of properties being sent into our child component. If, for example, we have 100s of properties stored in context, then we don't want to send all of them down to every component.
const Heading = ({ color, children }) => <h1 style={{ color }}>{children}</h1>
const contextTypes = {
color: PropTypes.string
}
const HeadingWithContext = getContext(contextTypes)(Heading);
Just as we do with Redux's connect
function, we can create an Object defining what we want from context, and our getContext
helper will fetch it and add it as props
to the component. Great!
recompose
can be used to create the above context HOC in a much cleaner fashion:
const Provider = () => withContext(
{ store: PropTypes.object },
props => ({ store: props })
)(props => React.Children.only(props.children));
const connect = Component => getContext(
{ store: PropTypes.object }
)(Component);
export { Provider, connect };
Much simpler. Our Provider will work exactly the same, but our getContext
helper can be refactored into a much cleaner connect
helper. It uses getContext
to deconstruct the context
Object and get context.store
, and then pass that in as props
for the component.
How to use it?
const App = () => <Home />
const Home = connect(({ store }) => <h1>{store.app.title}</h1>)
render(
<Provider app={{ title: 'recompose is cool' }}>
<App />
</Provider>,
document.getElementById('app')
)
Just like that, we have virtually implemented react-redux
. In fact, if we really did want to implement react-redux
we'd only have to change our connect
helper to the following for it to work:
const connect = mapStateToProps => Component => compose(
getContext({ store: PropTypes.object }),
mapProps(props => ({ ...props, ...mapStateToProps(props.store) })),
)(Component);
// Usage
const mapStateToProps = store => ({
title: store.app.title
})
export default connect(mapStateToProps)(Home)
Now, our connect
helper will first get the context, and then use our mapStateToProps
function to map the required props to the component. It's not perfect, as it currently requires a mapStateToProps
function to be passed in, but the basic idea is there at least.
// Suspens lets your components "wait" for something before rendering
const resource = fetchProfileData();
function ProfilePage() {
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline />
</Suspense>
</Suspense>
);
}
function ProfileDetails() {
// Try to read user info, although it might not have loaded yet
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
function ProfileTimeline() {
// Try to read posts, although they might not have loaded yet
const posts = resource.posts.read();
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
// The implementation of fetchProfileData() is not that important, but it will return
// something like below:
return {
read() {
if (status === "pending") {
throw suspender;
} else if (status === "error") {
throw result;
} else if (status === "success") {
return result;
}
}
};
Libraries like Relay will have their own integration with Suspense to work with this syntax. In the future, other libraries may have their own implementation. The point is that it throws a "suspender" when the resource is still reading, which under-the-hood will allow the component to continue rendering. Of course, the fallbacks will show where you add them, but it allows you to render "most" of the page before the data has even started fetching.
Suspense is not:
- Data fetching implementation
- Couple data fetching with your view layer. It helps display loading states in your UI, but it does not tie network logic to React components.
Suspense lets you:
- Deeply integrate data fetching libraries with React. If a data fetching library is integrated with Suspense, using it in your components feels very natural.
- Avoid race conditions. Avoid error-prone async code. Suspense "feels" like reading data synchronously, as if it was already loaded.
Fetch-on-render (e.g. fetch in useEffect):
- Start rendering components
- Each component triggers a fetch in their useEffects
- Often loeads to a waterfall effect of data fetching/loading
function ProfilePage() {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(u => setUser(u));
}, []);
if (user === null) {
return <p>Loading profile...</p>;
}
return (
<>
<h1>{user.name}</h1>
<ProfileTimeline />
</>
);
}
function ProfileTimeline() {
const [posts, setPosts] = useState(null);
useEffect(() => {
fetchPosts().then(p => setPosts(p));
}, []);
if (posts === null) {
return <h2>Loading posts...</h2>;
}
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
Running this code results in:
- Start fetching user and show loading until a user has been returned
- Once it's finished, this will trigger ProfileTimeline to render and start loading posts
- Finally, both user and posts will be rendered
This is an unintentional waterfall sequence that could, and should have, been parallelized. As the app grows, this kind of behaviour could become more and more troublesome for the user.
Fetch-then-render
function fetchProfileData() {
return Promise.all([
fetchUser(),
fetchPosts()
]).then(([user, posts]) => {
return {user, posts};
})
}
const promise = fetchProfileData();
function ProfilePage() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState(null);
useEffect(() => {
promise.then(data => {
setUser(data.user);
setPosts(data.posts);
});
}, []);
if (user === null) {
return <p>Loading profile...</p>;
}
return (
<>
<h1>{user.name}</h1>
<ProfileTimeline posts={posts} />
</>
);
}
// The child doesn't trigger fetching anymore
function ProfileTimeline({ posts }) {
if (posts === null) {
return <h2>Loading posts...</h2>;
}
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
Similar to the last example, this waits until data has been fetched to render something for the user. Here, the API calls are grouped together, which would likely happen if you use GraphQL or any kind of request batching. We still wait, but the 2 API calls are ran in parallel.
Render-as-you-fetch (with Suspense)
Previously we wouuld start fetching, finish fetching and start rendering. The goal with Suspense is to start fetching, start rendering and then finish fetching. We want the UI to render as we are fetching data so the user sees something as soon as possible.
// This is not a Promise. It's a special object from our Suspense integration as in the
// above simplified example.
const resource = fetchProfileData();
function ProfilePage() {
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline />
</Suspense>
</Suspense>
);
}
function ProfileDetails() {
// Try to read user info, although it might not have loaded yet
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
function ProfileTimeline() {
// Try to read posts, although they might not have loaded yet
const posts = resource.posts.read();
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
- As soon as ProfilePage renders, the fetching is kicked off and it tries to render both the ProfileDetails and the ProfileTimeline.
- React will try to render ProfileDetails using "resource.user.read()". While the resource fetching, the library will throw a "suspender" and "suspend" the component.
- Similarly, React will try to render ProfileTimeline and the same thing will happen when initially reading "resource.posts.read()".
- Whenever a component is "suspended", it will try to use the "fallback" of the closest Suspense component above it in the tree.
- Initially we will see "loading profile..." while ProfileDetails is "suspended"
function ProfilePage({ id }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(id).then(u => setUser(u));
}, [id]);
if (user === null) {
return <p>Loading profile...</p>;
}
return (
<>
<h1>{user.name}</h1>
<ProfileTimeline id={id} />
</>
);
}
function ProfileTimeline({ id }) {
const [posts, setPosts] = useState(null);
useEffect(() => {
fetchPosts(id).then(p => setPosts(p));
}, [id]);
if (posts === null) {
return <h2>Loading posts...</h2>;
}
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
Suppose that we have a button outside of the ProfilePage which will update the user ID that we want to show. Now, it's quite possible that we end up showing posts/profile data for two completely different users.
Solving it with Suspense:
const initialResource = fetchProfileData(0);
function App() {
const [resource, setResource] = useState(initialResource);
return (
<>
<button onClick={() => {
const nextUserId = getNextId(resource.userId);
setResource(fetchProfileData(nextUserId));
}}>
Next
</button>
<ProfilePage resource={resource} />
</>
);
}
function ProfilePage({ resource }) {
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails resource={resource} />
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline resource={resource} />
</Suspense>
</Suspense>
);
}
function ProfileDetails({ resource }) {
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
function ProfileTimeline({ resource }) {
const posts = resource.posts.read();
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
- We've moved the resource into the App component
- When the user ID is updated, it passes down the new "resouce" to the children
- As before, this new fetch will start straight away and the UI will also render straight way.