Soup in black and white, logo

ProgrammingSoup

Concise Guide to Advanced React

by Nicklas Envall

This article is made for developers who prefer a concise explanation. This article covers the following concisely:

  1. Render Props
  2. Context
  3. Fragments
  4. Higher-Order Components
  5. Portals
  6. Refs
  7. 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)}/>
  );
}