Concise Guide to Advanced React
by Nicklas EnvallThis article is made for developers who prefer a concise explanation. This article covers the following concisely:
- Render Props
- Context
- Fragments
- Higher-Order Components
- Portals
- Refs
- Controlled Components
To go more in-depth, I recommend https://reactjs.org/docs. The following examples are created for illustration purposes.
1. Render Props
Render prop is a technique that involves passing a function that returns a React element, to another component via a prop. Then, the passed function gets invoked inside that component. Meaning, render props allows us to share logic from a parent to a child component.
For example, in the code below, how can ChildComponent
access the state of ParentComponent
?
const ParentComponent = (props) => ( <div> {props.children} </div> ); const ChildComponent = () => <p>hello world</p>; const App = () => ( <ParentComponent> <ChildComponent/> </ParentComponent> );
One solution is, render props. In the example below, we pass a function that returns ChildComponent
to the ParentComponent
as a prop, which then calls the function.
const ParentComponent = props => ( <div> {props.children('hello world')} </div> ); const ChildComponent = props => <p>{props.text}</p>; const App = () => ( <ParentComponent> {text => <ChildComponent text={text} />} </ParentComponent> );
2. Context
Context allows us to access data in child components without having to pass props through many nested levels of components. Essentially we want to avoid having to do this:
const FirstComponent = () => ( <div> <SecondComponent SomeData={'hello'} /> </div> ); const SecondComponent = (props) => ( <div> <ThirdComponent SomeData={props.SomeData} /> </div> ); const ThirdComponent = (props) => ( <div> {props.SomeData} </div> );
First we must create a context object with React.createContext(defaultValue)
. The context object has an attribute called Provider
- which is a component that looks like <Provider value={}>
. All components that have the Provider
component as an ancestor will have the value prop of the nearest ancestor provider as their context value. They use the default value if they are not nested in a provider.
In the example below, we use useContext
from the Hook API to access the current context value by passing it the context object. Our app
component uses the default value of the context object because it's not nested inside a Provider
. Furthermore, when a Provider
component updates then its nested children's hooks will trigger a rerender.
import React, { createContext, useContext } from 'react'; const MyContext = createContext('Hello World 1'); const App = () => { const ContextValue = useContext(MyContext); return ( <MyContext.Provider value={'Hello World 2'}> <SecondComponent/> <p>{ContextValue}</p> {/* Hello World 1 */} </MyContext.Provider> ); } // Note how we do not need to pass a prop here const = SecondComponent = () => ( <div> <ThirdComponent /> </div> ); const ThirdComponent = () => { const ContextValue = useContext(MyContext); return ( <p> {ContextValue} {/* Hello World 2 */} </p> ); }
3. Fragments
Fragments let us group nodes without adding extra nodes. If you've ever been frustrated that you have to add a div
or an extra element just to be able to return a group of nodes, then fragments are a great solution.
Now we'll look at two examples, one without fragments and one with fragments, that demonstrates how fragments work.
3.1 Without fragments:
const App = () => ( <div> <ContentComponent/> </div> ); const ContentComponent = () => ( <div> <h1>Hello World!</h1> <p>Hello again</p> </div> );
Would result in the following markup:
<div> <div> <h1>Hello World!</h1> <p>Hello again</p> </div> </div>
3.2 With fragments:
const App = () => ( <div> <ContentComponent/> </div> ); const ContentComponent = () => ( <React.Fragment> <h1>Hello World!</h1> <p>Hello again</p> </React.Fragment> );
Would result in the following markup:
<div> <h1>Hello World!</h1> <p>Hello again</p> </div>
4. Higher-Order Components
A higher-order component is a function that takes a component as an argument and then returns a new component. Essentially, HOCs are wrappers for components. Most often, we prefix the names of our HOCs with with
or get
.
// higher order component const withStyle = Component => ( class extends React.Component { render() { return <Component style={{ background: 'blue'}} /> } } ); // the component we'll pass to withStyle const HelloWorld = props => <p {...props}>HelloWorld</p>; // pass HelloWorld and get a new component back const StyledHelloWorld = withStyle(HelloWorld); const App = () => ( <div> <StyledHelloWorld/> </div> );
HOCs can make our code more DRY. Furthermore, you should know that you do not use HOCs inside a render method, you can read more about that here.
5. Portals
Portals allow us to render a child component into a DOM node that is not a descendant of the child's parent. Styling is a common reason for using portals when the ancestral hierarchy is hindering a specific style for an element.
By looking at two examples, we can more easily understand what this means.
5.1 Without portal:
Let's setup our index.html
:
<!-- index.html --> <html> <body> <div id="root"></div> </body> </html>
Then we'll create a simple app in index.js
:
// index.js import React from 'react'; import ReactDOM from 'react-dom'; const appRoot = document.getElementById('root'); const App = () => ( <div> <h1>hello world</h1> <p>hello again</p> </div> ); ReactDOM.render(<App />, appRoot);
As expected, this will render:
<html> <body> <div id="root"> <div> <h1>hello world</h1> <p>hello again</p> </div> </div> </body> </html>
5.2 With portal:
With a portal, we can render the p
tag outside of the root. Let's update the index.html
file:
<html> <body> <div id="root"></div> <div id="other-root"></div> </body> </html>
Let's also update the index.js
file:
import React from 'react'; import ReactDOM from 'react-dom'; const appRoot = document.getElementById('root'); const otherRoot = document.getElementById('other-root'); const App = () => ( <div> <h1>hello world</h1> {ReactDOM.createPortal(<p>hello again</p>, otherRoot)} </div> ) ReactDOM.render(<App />, appRoot);
Now this will render:
<html> <body> <div id="root"> <div> <h1>hello world</h1> </div> </div> <div id="other-root"> <p>hello again</p> </div> </body> </html>
A useful and mildly confusing thing is that Event Bubbling will work as if the element still is a descendant of the parent. In other words, event bubbling for the React child that you teleported to another DOM Node will work as if it never was teleported.
For example, if you click on the button in the code below, it'd console.log <button>hello again</button>
:
const App = () => ( <div onClick={(event) => console.log(event.target)}> <h1>hello world</h1> {ReactDOM.createPortal(<button>hello again</button>, otherRoot)} </div> );
6. Refs
Refs allow us to access DOM nodes or React elements created in the render method. First, we must create a ref with either createRef
or the useRef
hook. The second thing is to pass the ref via a prop to the element of which DOM node we want to access. For example:
const App = () => { const pElement = useRef(null); return ( <div> <h1>Hello World</h1> <p ref={pElement}>Hello again</p> </div> ) };
Now pElement
has the attribute current
which is the reference to its DOM node. You can try out the following code on your own - to get an idea:
const App = () => { const pElement = useRef(null); return ( <div> <h1>Hello World</h1> <p ref={pElement} onMouseOver={e => console.log(pElement.current)} > Hello again </p> </div> ) }
Refs are an easy way to access DOM nodes but you should use them sparingly, overusing them is a sign that you're on the wrong path. We should also keep in mind that ref
is a reserved prop for all standard HTML elements in React. Lastly, we cannot pass refs to function components via ref
, but we can use forwardRef. So, if you need to pass a ref with the ref
attribute to a child component, use forwardRef
like this:
const FuncComponent = React.forwardRef((props, ref) => <div ref={ref}/>); const App = () => { const myRef = useRef(null); return <FuncComponent ref={myRef} /> }
7. Controlled components & Uncontrolled components
We both have controlled and uncontrolled components. An uncontrolled component has an internal state that keeps track of a value, while a controlled component uses props.
Form elements such as <select>
, <input>
and <textarea>
all have their own internal state, which means they are by default uncontrolled. The input element below has its own internal state for handling the value:
const App = () => <input type="text" />;
When we pass a value via the value
prop it becomes a controlled component, because it now uses React's state. This is because the value attribute overwrites the form element's value in the DOM. The state of the value gets updated with callbacks like onChange
.
const App = () => { const [value, setValue] = useState('hello world'); return ( <input type="text" value={value} onChange={(event) => setValue(event.target.value)}/> ); }