React 17: Design Patterns and Best Practices - Notes

React 17: Design Patterns and Best Practices - Notes

I recently read React 17: Design Patterns and Best Practices. I was able to add a lot to my React toolset. Here are ALL my takeaways (will likely have a condensed version for the most important takeaways in the future).

**Note - most of the text in this document is taken directly from the book. These are notes/quotes, not my own content.

React Overview

Declarative vs Imperative

React enforces a declarative programming paradigm.

Imperative describes how things work and all the instructions are included. Like giving instructions to a bartender:

  1. Find a glass and collect it from the shelf

  2. Place the glass under the tap

  3. Pull down the handle until the glass is full

  4. Hand me the glass

While declarative is just describing the outcome - "Can I have a beer, please?"

// Imperative approach
const toUpperCase = input => {
    const output = []
    for (let i = 0; i < input.length; i++) {
        output.push(input[i].toUpperCase());
    }
    return output;
}

// Declarative approach
const toUpperCase = input => input.map(value => value.toUpperCase())

In declarative programming, developers only describe what they want to achieve, and there's no need to list all the steps to make it work. React handles the rest.

TypeScript Types

Types help us to describe our objects or arrays in a better way.

type TUser = {
    username: string
    name: string
    age: number
    email: string
}

It is good practice to preface your types with T (TUser)

TypeScript Interfaces

Interfaces are very similar to types, and sometimes developers don't know the differences between them. Syntax is a little different from types

inteface IUser {
    username: string
    name: string
    age: number
    email: string
}

It is good practice to preface interfaces with I (IUser)

An interface can be defined multiple times and will be treated as a single interface (all declarations will be merged)

interface IUser {
    username: string
    name: string
}
interface IUser {
    country: string
}
const user: IUser = {
    username: 'chad'
    name: 'michael murray'
    country: 'croatia'
}

You can also extend interfaces and types, but the syntax is different:

interface IWork {
    company: string
    position: string
}
interface IPerson extends IWork {
    name: string
    age: number
}

type TWork = {
    company: string
    position: string
}
type TPerson = TWork & {
    name: string
    age: number
}

Styling/Formatting

Align closing tag with opening tag

<button
    foo="bar"
    veryLongPropertyName="baz"
    onSomething={this.handleSomthing}
/>

Ternaries make the code more compact

<div>
    {isLoggedIn ? <LogoutButton /> : <LoginButton />
</div>

Using helper functions for conditions

Avoid polluting our code with conditionals so that it is easier to understand and reason about

const canShowSecretData = () => {
    return dataIsReady && (isAdmin || userHasPermissions);
}
return (
    <div>
        {canShowSecretData() && <SecretData />}
    </div>
)

Plugin for conditionals: jsx-control-statements

Not entirely sure how I feel about this solution, but it is interesting nonetheless.

npm install --save jsx-control-statements

(had some issues actually implementing this... deprecated?)

// Gets compiled into a ternary expression
<If condition={this.canShowSecretData}>
    <SecretData />
</If>

// Gets compiled into multiple ternary expressions
<Choose>
    <When condition={...}>
        <span> if </span>
    </When>
    <When condition={...}>
        <span> else if </span>
    </When>
    <Otherwise>
        <span> else </span>
    </Otherwise>
</Choose>

Prettier will format your code

Avoid doing this in code reviews

Inline Styles

In general, we should avoid inline styles. They make it difficult to debug, as the CSS is no longer in classes, and directly into the markup. With lots of elements doing this, it quickly gets out of hand and is difficult to find.

It is impossible to use pseudo-selectors (:hover ) with inline styles.

However, they do offer some unique benefits for special circumstances. The main one is that we can calculate and recalculate CSS values on the client at runtime.

height of div is dependent upon state and values at runtime
const style {
    height: someStateValue * 10
}

<div style={style}>
    ...
</div>

Radium Library

Radium is a library that allows you to use inline styles with pseudo-classes using Radium HOC's

import Radium from 'radium'
const Button = () => <button>Click Me!</button>
export defualt Radium(Button)

Atomic CSS Modules

using micro-classes to build the styles of components

.mb0 {
    margin-bottom: 0;
}
.fw6 {
    font-weight: 600;
}

// Apply to markup
<h2 class="mb0 fw6">Hello React</h2>

Similar to Tailwind CSS. I am not a big fan of this.

Styled Components

Use the styled function to create any element by using styled.elementName, where elementName can be a div, a button, or any other valid DOM element.

Then define the style of the element by using an ES6 feature called tagged template literals.

const Button = styled.button`
    backgroundColor: #FFFFFF;
    width: 320px;
    padding: 20px;
    borderRadius: 5px;
    &:hover {
        color: #fff;
    }
    &:active {
        position: relative;
        top: 2px;
    }
`

This also allows for pseudo-classes, but when it is compiled, it creates a unique class name and adds it to the element, then injects the corresponding style in the head of the document.

// as if it were like so
<button class="kYvFOg">Click Me!</button>

// styles
.kYvFOg {
    backgroundColor: 'red';
    ...
}

I don't fully understand this, but it is powerful.

Programming Concepts

Functional Programming

Data is considered immutable and side effects are avoided

High-Order Functions

Functions that take a function as a parameter, and optionally some other parameters, and return a function. The returned function is usually enhanced with some special behaviors.

const add = (x, y) => x + y

const log = fn = (...args) => {
    return fn(...args)
}
const LogAdd = log(add)

Purity

A function is pure when there are no side effects, which means that the function does not change anything that is not local to the function itself.

Currying

The process of converting a function that takes multiple arguments into a function one argument at a time and returning another function.

// Say we have the following code
const add = (x, y) => x + y

// We can instead define the function as follows
const add = x => y => x + y

// We use it the following way
const add1 = add(1)
add1(2)    // 3
add1(3)    // 4

Composition

Small, simple, testable pure functions that can be composed together

const add = (x, y) => x + y
const square = x => x * x

const addAndSquare = (x, y) => square(add(x, y))

Hooks

FC Type

Used to define a Functional Component in React. Can pass in props like this

type Props = {
    propX: string
    propY: string
    propX: string
}
const Issues: FC<Props> = ({ propX, propY, propX }) => {...}

UseEffect

Handles all the previous life cycle events - componentDidMount, componentDidUpdate, componentWillUnmount - depending on the syntax of useEffect()

// Equivalent of componentDidMount() method
// runs upon rendering/mounting component
useEffect(() => {
    // Perform side effect here
}, []);

// This is kind of a combination of componentDidMount, componentDidUpdate,
// and componentWillUnmount
useEffect(() => {
    // This code will run when any of the dependencies in the array
    // change or are updated
}, [depA, depB]);

Using functions inside useEffect

Every time you want to use a function inside the useEffect Hook, you need to wrap your function with the useCallback Hook

const fetchIssues = useCallback(async () => {
    const response = await axios(url)
    if (response) setIssues(response.data)
}, [url]);

useEffect(() => {
    fetchIssues()    // is wrapped with useCallback
}, []);

useRef

There might be cases where you need to access the underlying DOM nodes to perform some imperative operations. This should be avoided if possible.

Children Prop

There is a special prop that can be passed from the owners to the components defined inside their render - children.

It is described as opaque because it is a property that does not tell you anything about the value it contains. Components can also be defined with nested components inside them, and they can access those children using the children prop.

Consider the following scenario:

// Button that has a TEXT property for the display text
const Button = ({ text }) => (
    <button className="btn">{text}</button>
)

// Can then be used in the following way
<Button text="click me!" />

// Which would then be rendered as:
<button class="btn">click me!</button>

But what if we want to display more than just a string in this button? Icons, images, labels, etc.

We could add multiple parameters to Button and include logic to handle each of the specialized cases.

Or, we can use the children prop and make Button just a wrapper.

const Button = ({ children }) => (
    <button className="btn">{children}</button>
)

In this case, any element we wrap inside the Button component will be rendered as a child of the button element with btn as the class name.

<Button>
    <img src"..." alt="..." />
    <span>Click me!</span>
</Button>

// would then be rendered as 
<button class="btn">
    <img src"..." alt="..." />
    <span>Click me!</span>
</button>

Container and Presentational Patterns

React components typically contain a mix of logic and presentation. This pattern helps us to separate these concerns. This can help the component become more reusable.

One file for the presentation (anything inside the render method), one file for the logic (helpers, API calls, data transformations, etc.).

HOC's (High-Order Components)

React Context API

Provides a way to share data between components without passing a prop to all the child components.

To consume the context (use the Context API), we need to wrap the component with a Provider.

const App = () => {
    return (
        <IssueProvider url="...someURL">
            <Issues />     // this is where we want to consume api
        </IssueProvider>
    )
}

IssueProvider might look something like this

const IssueProvider: FC<Props> = ({ children, url }) => {
    // State
    const [issues, setIssues] = useState<Issue[]>([])

    const fetchIssues = useCallback(async () => {
        const response = await axios(url)
        if (response) setIssues(response.data)
    }, [url]);

    useEffect(() => {
        fetchIssues()
    }, []);

    const context = {
        issues,
        url
    }

    return <IssueContext.Provider value={context}>{children}</IssueContext.Provider>
}
export default IssueProvider

And you might consume the Context API like this

import { FC, useContext } from 'react'
import { IssueContext, Issue } from './contexts/Issue'

const Issues: FC = () => {
    // Here you consume Context, grab the issues val
    const { issues, url } = useContext(IssueContext)

    return (
        <>
            issues.map((issue: Issue) => (
                ... // do something with issues from Context
            )
        </>
    )
}

Generic Event Handlers

We might run into scenarios where a component has multiple event handlers, and a lot of boilerplate code comes along with them. In these situations, we can implement a single, generic event handler like so:

const handleEvent = (event) => {
    switch (event.type) {
        case: 'click':
            console.log('clicked');
            break
        case: 'dblclick':
            console.log('double clicked');
            break
        defualt:
            console.log('unhandled', event.type)
    }
}

// Attach event listener to onClick and onDoubleClick attributes
return (
    <button
        onClick={handleEvent}
        onDoubleClick={handleEvent}
    >
        Click Me!
    </button>
)

Reconciliation

The React Library implements different techniques to render our components fast and to touch the Document Object Model (DOM) as little as possible. Applying changes to the DOM is usually expensive, so minimizing the number of operations is crucial.

When React has to display a component, it calls its render method and the render methods of it children recursively. The render method of a component returns a tree of React elements, which React uses to decide which DOM operations have to be done to update the UI.

Whenever the component state changes, React calls the render method on the nodes again, and it compares the result with the previous tree of React elements. The library is smart enough to figure out the minimum set of operations required to apply the expected changes on the screen. This process is called reconciliation, and it is managed transparently by React.

Keys and Reconciliation

// Lets say we have a list of two items
<ul>
    <li>Carlos</li>
    <li>Javier</li>
</ul>

// Now lets add an item at the end of the list
<ul>
    <li>Carlos</li>
    <li>Javier</li>
    <li>Emmanuel</li>
</ul>

When we add an element to the end of this list, and React needs to re-render, it looks at the two trees and notices that the two trees are the same, so it will keep the two trees and insert the <li>Emmanuel</li> tree.

But when we add the item to the beginning of the list...

<ul>
    <li>Emmanuel</li>
    <li>Carlos</li>
    <li>Javier</li>
</ul>

React looks at the two trees and notices that they are not the same, so it re-renders the entire tree. Instead of realizing it can keep the subtrees of <li>Carlos</li> and <li>Javier</li> intact. This is quite a difference in optimization.

This is solved with the key attribute.

<ul>
    <li key="2018">Carlos</li>
    <li key="2019">Javier</li>
</ul>

<ul>
    <li key="2017">Emmanuel</li>
    <li key="2018">Carlos</li>
    <li key="2019">Javier</li>
</ul>

React knows that the 2017 key is the new one, and that the 2018 and 2019 keys have just moved.

Finding a key is not hard. The element that you will be displaying might already have a unique ID. So the key can just come from your data.

<li key={element.id}>{element.title}</li>

The key has to only be unique among its siblings. An item index in the array can be passed as a key, but it is considered bad practice. However, if the items are never reordered, this can work well. The reorders will seriously affect performance.

Immutability

The new React hooks, such as React.memo, use a shallow comparison method against the props, which means that if we pass an object as a prop and we mutate one of its values, we do not get the expected behavior.

In fact, a shallow comparison cannot find mutation on the properties and the components never get re-rendered, except when the object itself changes.

const [state, setState] = useState({})
const obj = state.obj;
obj.foo = 'bar';
setState({ obj })

The above code will not set the state as expected. Even if the value of foo is changed, the reference to the object is still the same and the shallow comparison does not recognize it.

Instead, we need to create a new instance every time we mutate the object.

const obj = Object.assign({}, state.obj, { foo: 'bar' })
setState({ obj })

And with ES6 and Babel, we can use the object spread operator:

const obj = { ...state.obj, foo: 'bar' }
setState({ obj })

immutable.js is a popular library with some immutability helpers that make it easy to work with immutable objects.

React DevTools

Installing React DevTools for Chrome allows you to inspect the rendered tree of components and check which properties they have received and what their state is at a particular point in time.

Props and states can be read, and they can be changed in real time to trigger updates in the UI and see the results straight away.

There is a new feature that can be enabled by ticking the Trace React Updates checkbox. When this functionality is enabled, we can use our application and visually see which components get updated when we perform a particular action. The updated components are highlighted with colored rectangles, and it becomes easy to sport possible optimizations

React Router

There is plenty in the book about React Router, but I will only include a small takeaway, something that was new and important to me.

Using the <Link> component:

<Link to={`/contacts/${contact.id}`}>{contact.name}</Link>

The <Link> component will generate an <a> tag that points to /contacts/contact.id

Then we will have something like the following in the AppRoutes Component:

Route path="/contacts/:contactId" component={Contacts} exact/>

const AppRoutes = () => {
    <App>
        <Switch>
            <Route path="..." ... />
            <Route path="..." ... />
            <Route path="/contacts/:contactId" component={Contacts} exact/>
        </Switch>
    </App>
}

React Router has a special prop called match, which is an object that contains all the data related to the route, and if we parameters, we will be able to see them in the match object:

const Contacts: FC<Props> = (props) => {
    const { match: { params: { contactId } } } = props;
    // can now use contactId as var
}

The match prop looks like this:

match: {
    isExact: true
    params: { contactId: "2" }
    path: "/contacts/:contactId"
    url: "/contacts/2"
    __proto__: Object
    staticContext: undefined
}

Server Side Rendering (SSR)

SSR should only be enabled when strictly necessary. For example, if you need SEO (Search Engine Optimization) or if you need to customize the social sharing information, you should start thinking about it.

AntiPatterns to be avoided

Initializing the state using properties

Ex: const [count, setCount] = useState<number>(props.count);

This has two main issues:

1 - We have a duplicated source of truth

This makes it unclear which is the current and trustworthy value to use inside the component and to display to the user. The values can diverge...

<Counter>
Props:
    count: 1
State:
    count: 2

2 - If the count property passed to the component changes, the state does not get updated

If the value of the property changes during the life cycle of the application (let's say it becomes 10), the Counter component will never use the new value, because it has already been initialized.

Using Indexes as a Key

This has already been discussed in the Reconciliation section.

Simply put, using the wrong key can give us unexpected behaviors in some instances, and can affect performance.

The key has to be unique and stable

Contributing to React

Share your React code

Sharing your code forces you to follow best practices and write better code. It also exposed your code to feedback and comments from other developers. This is a big opportunity for you to receive tips and improve your code to make it better.

Another significant opportunity that open source can give you is letting you get in touch with smart and passionate developers from all around the world. Working closely with new people who have different backgrounds and skill sets is one of the best ways to keep our minds open and improve ourselves.

Publishing an npm package

The most popular way of making a package available to developers is by publishing it to npm, the package manager for Node.js.

Semantic Versioning:

v 0.1.0

The last number of the package on the right represents the patch. The middle number indicates the minor version of the release and it should be changed when new features are added to the library. The first number on the left represents the major version, and it has to be increased when a version containing breaking changes is released to the public.

Glossary

JWT - JSON Web Token

Imperative Programming - Describing how things work

Declarative Programming - Describing what you want to achieve. Being declarative means that you just describe what you want to be displayed on the screen at any point in time and React takes care of the communications with the browser.

Uncontrolled Component - like regular HTML form inputs for which you will not be able to manage the value yourself but instead, the DOM will take care of handling the value and you can get this value by using a React ref.

Controlled Component - a React component that controls the values of input elements in a form by using the component state. Full control over the values in the fields