Skip to main content

React

Version


We are currently using the latest version of React Hooks.

Prerequisites


If you are not familiar with React Hooks, please take a moment to read the official documentation.

React Hooks


The main Hooks that you will need to at least be aware of are:

  • useState
  • useEffect
  • useCallback
  • useMemo
  • Custom Hooks

Below you will find a simple explanation of how we prefer to use them within the project.

REMEMBER that this is more of a guide to keep everything normalized. If you find technical implications or improvements that we could use, you're more than welcome to use them. Just remember to come to an agreement with the team and update the docs!

useState


As recommended by the official docs, destructure the returned array and extract the state value and the setter function in a single line.

Set a representative name to the state value like myValue and for the setter function just prepend a set to that name like setMyValue

const [myValue, setMyValue] = useState(1);

useEffect


React's official documentation presents the useEffect hook examples as follows:

useEffect(() => {
// implementation
}, []);

It calls the hook sending an arrow function and an array of dependencies. If you pay attention, you don't have any clue of what's the effect doing. To understand what's happening, you are forced to read all the code within the arrow function.

To avoid this problem, and give other developers a sense of what our useEffect is doing, we prefer to use them as follows:

useEffect(function callingAPI {
// implementation
}, [])

Instead of using an arrow function as first params of the hook, just use a standard function with a representative name of what's happening within.

Array of Dependencies


Simple definition taken from some blog: In the simplest terms, useEffect is a Hook that allows you to perform side-effects in functional components. For even more detail, these effects are only executed after the component has rendered, therefore not blocking the render itself.

This is the place where most of React devs make mistakes. The array of dependencies should only contain every variable | function | symbol, that IS BEING USED within the hook's function and IS DEFINED OUTSIDE the hook's function scope.

Let's go with an example:

Take a look at the useEffect, and think which ones should be the dependencies.

export const PokemonRandomizer = ({ title, findRandomPokemon, pokemon }) => {
const randomNumber = getRandomNumber();

useEffect(
function fetchPokemon() {
// don't worry about the calcs
const pokemonId = randomNumber < 100 ? randomNumber : randomNumber % 100

findRandomPokemon(pokemonId)
}, [
// empty on purpose. Think.
])

return ...
}

What are we using within the hook's function that is not defined within? If you answered findRandomPokemon and randomNumber, you are correct!

export const PokemonRandomizer = ({ title, findRandomPokemon, pokemon }) => {
const randomNumber = getRandomNumber();

useEffect(
function fetchPokemon() {
// don't worry about the calcs
const pokemonId = randomNumber < 100 ? randomNumber : randomNumber % 100

findRandomPokemon(pokemonId)
}, [
findRandomPokemon,
randomNumber
])

return ...
}

But what about pokemonId? Well, think of the second part of the definition above: "IS DEFINED OUTSIDE the hook's function scope"

When this effect will be re-triggered?

This effect will run on every (or almost every) re-render of the component. Why? Let's think about the dependencies:

  • findRandomPokemon: is a function defined outside. Its reference won't change, so it will not cause the effect to trigger again.

  • randomNumber: well, we expect to get a random number. So on each re-render, we should always (there's a possibility where we get the same number than before) get a different number than the previous render, causing the effect to re trigger.

How can we avoid that?

Well, depends on what we want. If we want to trigger this just once, then we could move the randomNumber getter inside the hooks effect, as follows:

export const PokemonRandomizer = ({ title, findRandomPokemon, pokemon }) => {
useEffect(
function fetchPokemon() {
const randomNumber = getRandomNumber();

// don't worry about the calcs
const pokemonId = randomNumber < 100 ? randomNumber : randomNumber % 100

findRandomPokemon(pokemonId)
}, [
findRandomPokemon,
getRandomNumber
])

return ...
}

This way we avoid the dependency on randomNumber on the effect, and save it from changing on every re-render. Realize we now have a different dependency, getRandomNumber. Because we are calling that function within the hook's function, and is a function defined outside its scope.

So, what happens when the function re-triggers an effect? Can that happen?

Of course it can happen! Here is where the famous useCallback comes to play, that not many devs know.

Understanding the array of dependencies and its implications with values that work by reference


Functions | objects | arrays work by reference on JS. If you assign any of these to a variable, you are saving a reference to that value on memory. Not the actual value.

NOTE: We are going to be using functions as the subject to analyze, but same rules apply to objects and arrays

For example:

export const PokemonRandomizer = ({ title, findRandomPokemon, pokemon }) => {

const getRandomNumber = () => Math.floor(Math.random() * 100)

useEffect(
function fetchPokemon() {
const randomNumber = getRandomNumber();
findRandomPokemon(randomNumber)
}, [
findRandomPokemon,
getRandomNumber
])

return ...
}

In this case, when the PokemonRandomizer component gets rendered, JS will create a new function on memory and assign its reference to getRandomNumber.

So far, so good. But, think about this:

What will happen on a following re-render?

JS will create a new function on memory and assign its reference to getRandomNumber

Yes, again the same! Look at the component definition, it's just a normal function. So, this means that:

+ EACH RENDER OR RE-RENDER OF A COMPONENT, IT'S RE-EXECUTING THE COMPONENT'S FUNCTION AS SIMPLE JS

All JS rules apply here.

If we define a function within a function:

const myFunc = () => {
const anotherFunc = () => {};
return anotherFunc;
};

We will get a different reference on each execution of the outer function.

const anotherFunc1 = myFunc();
const anotherFunc2 = myFunc();

anotherFunc1 === anotherFunc2; // FALSE!

And this is exactly what happens with our React's components

Let's go back:

export const PokemonRandomizer = ({ title, findRandomPokemon, pokemon }) => {

const getRandomNumber = () => Math.floor(Math.random() * 100)

useEffect(
function fetchPokemon() {
const randomNumber = getRandomNumber();
findRandomPokemon(randomNumber)
}, [
findRandomPokemon,
getRandomNumber
])

return ...
}

Now think about the re-renders, the getRandomNumber, and the useEffect...

When will useEffect be re-triggered?

On every single render of the component, correct! Because, the way this is built, we get a new reference to a function on getRandomNumber, and getRandomNumber is a dependency of the useEffect, so React will detect a change, and re-trigger the effect.

Understood, so how do we avoid it?

Well, we have two options:

  • Either we move the function definition within the useEffect's function like:
export const PokemonRandomizer = ({ title, findRandomPokemon, pokemon }) => {

useEffect(
function fetchPokemon() {
const getRandomNumber = () => Math.floor(Math.random() * 100)

const randomNumber = getRandomNumber();
findRandomPokemon(randomNumber)
}, [
findRandomPokemon
])

return ...
}

This way we remove the function as the effect dependency.

  • Or we use another React hook useCallback

useCallback


Remember to read the official docs, to understand how the hook works.

Think about our example on the section before:

export const PokemonRandomizer = ({ title, findRandomPokemon, pokemon }) => {

const getRandomNumber = () => Math.floor(Math.random() * 100)

useEffect(
function fetchPokemon() {
const randomNumber = getRandomNumber();
findRandomPokemon(randomNumber)
}, [
findRandomPokemon,
getRandomNumber
])

return ...
}

We could solve our problem of reference changing, just by wrapping our function definition with useCallback, as follows:

export const PokemonRandomizer = ({ title, findRandomPokemon, pokemon }) => {

const getRandomNumber = useCallback(() => Math.floor(Math.random() * 100), []);

useEffect(
function fetchPokemon() {
const randomNumber = getRandomNumber();
findRandomPokemon(randomNumber)
}, [
findRandomPokemon,
getRandomNumber
])

return ...
}

When to use useCallback?


The rule of thumb that we follow is that we shouldn't apply useCallback to every function defined within a component.

Why not? Well, every useCallback that we use forces React to save that function on memory, and prevent it from being cleaned up, so we prevent JS from freeing up space on memory.

Instead, your first approach should be to NOT use useCallback, unless you need to. Meaning, that unless you add your defined function to an array of dependencies, then you should not use useCallback.

useMemo


Remember to read the official docs, to understand how the hook works.

We said on the beginning of the section "Understanding useEffect array of dependencies and its implications with values that work by reference" that functions, object and arrays work by reference.

On the section before, we talked about saving the issue of the reference changing for functions. So, how do we do it for objects and arrays?

Well, useMemo is the solution:

export const PokemonRandomizer = ({ title, findRandomPokemon, pokemon }) => {

const myMemoizedValue = useMemo(() => {
// return value to memoize
}, []);

useEffect(
function fetchPokemon() {
// ... some logic
}, [
findRandomPokemon,
myMemoizedValue
])

return ...
}

Same rules about memory leaks for useCallback apply for useMemo

When to apply useMemo?


If we have an array, that is defined within a component function and we need to place it as a dependency of an array, then only on that situation should we wrap the array definition with a useMemo.

export const PokemonRandomizer = ({ title, findRandomPokemon, pokemon }) => {

const pokemonsIds = useMemo(() => {
// expensive calculation
return [...]
}, []);

useEffect(
function fetchPokemon() {
pokemonsIds.forEach(pokemonId => findRandomPokemon(pokemonId))
}, [
findRandomPokemon,
pokemonsIds
])

return ...
}

If we have an object, that is defined within a component function and we need to place it as a dependency of an array, then we have two options:

  • Pass only the specific properties you are using from that object to the array of dependencies. As React uses strict comparison, if we send primitive values that are used by value, like strings, numbers, etc., the value to compare will be the same, although the object reference changes.
export const PokemonRandomizer = ({ title, findRandomPokemon, pokemon }) => {

const pokemon = {
id: '1',
name: 'Bulbasaur'
}

useEffect(
function fetchPokemon() {
findRandomPokemon(pokemon.id)
}, [
pokemon // WRONG
])

return ...
}
export const PokemonRandomizer = ({ title, findRandomPokemon, pokemon }) => {

const pokemon = {
id: '1',
name: 'Bulbasaur'
}

useEffect(
function fetchPokemon() {
findRandomPokemon(pokemon.id)
}, [
pokemon.id // GOOD
])

return ...
}
  • Wrap the object with a useMemo if you need to use the entire object for something.
export const PokemonRandomizer = ({ title, findRandomPokemon, pokemon }) => {

const pokemon = useMemo(() => {
return {
id: '1',
name: 'Bulbasaur'
}
}, []);

useEffect(
function fetchPokemon() {
findRandomPokemon(pokemon)
}, [
pokemon
])

return ...
}

Custom Hooks


Please, read the official docs to understand the concept and usage of Custom Hooks.

The two main advantages that we would like to leverage on Custom Hooks are:

  • Reusability: Any combination of useState, useEffect, useCallback, JS Logic, etc., that you need to apply in more than one component can be extracted to a custom hook.

  • Encapsulation: Complex components can get tedious to work with. So instead of throwing a ton of hooks within a single component, let's take them out on multiple custom hooks, to encapsulate their implementation, and make the component more readable.

Forms


First of, you will need to be familiar with Controlled vs Uncontrolled Components.

There are two ways of handling forms on any JS application:

  • Manually
  • With a forms library

For simple apps, where there are few forms to handle, we prefer to do so manually.

For larger apps, it may be worth it to analyze using a form library. We recommend React Hook Form for that.

How to handle manual validations?


To achieve manual validations on simple forms, with a small number of inputs, we will use Controlled Components. This means that we will apply our validations with JS logic leveraging the usage of Regular Expressions.

For example:

// MyForm.constants.ts
export const regex = {
username: /^[a-zA-Z\-]+$/,
password: /(?=.*[0-9])/
};

export const inputNames = {
USERNAME: 'username',
PASSWORD: 'password'
};

// MyForm.component.ts
import { regex, inputNames } from './MyForm.constants.ts';

export const MyForm = ({}) => {
const [input, setInput] = useState({});
const [error, setError] = useState({});

const handleInputChange = ({ currentTarget: { name, value } }) => {
const validatedValue = value.match(regex[name]);

if (validatedValue) {
setInput({ ...input, [name]: value });
}

setError({ ...error, [name]: !validatedValue });
};

return (
<form>
<div>
<label>Username:</label>
<input type="text" name={inputNames.USERNAME} onChange={handleInputChange} />

{error[inputNames.USERNAME] && <p>Only characters A-Z, a-z and '-' are acceptable.</p>}
</div>

<div>
<label>Password:</label>
<input type="text" name={inputNames.PASSWORD} onChange={handleInputChange} />

{error[inputNames.PASSWORD] && <p>The password must contain at least 1 numeric character.</p>}
</div>

<input type="submit" />
</form>
);
};

Simple, right?

API Calls


We recommend first reading the documents store and services to get familiar with those concepts.

Depending on the purpose of the API call, we will have two different ways to make them:

  • Information that needs to be reactive, in order to trigger changes on determined components: Reactive Data (Store)

  • Information that is only consumed by one component at a time: Static Data (No Store)

No matter the nature of the API call, they all share one piece of the implementation: Services. Every interaction with an API will be placed under a Service.

Reactive Data (Store)


Information that needs to be reactive, in order to trigger changes on determined components that are at the same time displayed on the screen.

This means that we want to fetch/send information that will provide input data for different unrelated components, that are all on the current rendered page. This input data will trigger a re-render of these components and refresh them in the view all together.

As this is information that needs to be reactive, it will end up saved on the Store.

Implementation


  1. Create a function to make the API Call under the module's service.
// pokemon-randomizer/services/pokedex.service.ts

class PokedexService {
axiosInstance: AxiosInstance;

constructor(axios) {
this.axiosInstance = axios;
}

getPokemonAPIRequest(id: number): Promise<Pokemon> {
return this.axiosInstance
.get(`https://pokeapi.co/api/v2/pokemon/${id}`)
.then((response: { data: Pokemon }) => response.data);
}
}
  1. Create a Thunk function under the module you are working on.
// pokemon-randomizer/store/pokemon-randomizer.thunks.ts

export const findRandomPokemon = (id: number): AppThunkAction => async (dispatch: AppThunkDispatch) => {
try {
// API call
} catch (err) {
// catch error
}
};
  1. The thunk function calls the service's function that makes the actual API call
// pokemon-randomizer/store/pokemon-randomizer.thunks.ts

import { pokedexService } from '../services/pokedex.service';

export const findRandomPokemon = (id: number): AppThunkAction => async (dispatch: AppThunkDispatch) => {
try {
const pokemon = await pokedexService.getPokemonAPIRequest(id);

// ...
} catch (err) {
// catch error
}
};
  1. The thunk dispatches through Action Creators the information to be reactive to the store
// pokemon-randomizer/store/pokemon-randomizer.thunks.ts

import { pokedexService } from '../services/pokedex.service';
import { setPokemon } from './pokemon-randomizer.actions';

export const findRandomPokemon = (id: number): AppThunkAction => async (dispatch: AppThunkDispatch) => {
try {
const pokemon = await pokedexService.getPokemonAPIRequest(id);

dispatch(setPokemon(pokemon));
} catch (err) {
// catch error
}
};
  1. Inject the Thunk into the component you want to trigger the API call through a container
// pokemon-randomizer/PokemonRandomizer.container.ts

import { connect } from 'react-redux';
import { AppReducerState } from '../../App.reducers';
import { PokemonRandomizer } from './PokemonRandomizer.component';
import { findRandomPokemon } from './store/pokemon-randomizer.thunks';

const mapStateToProps = (state: AppReducerState) => ({});

const mapDispatchToProps = {
findRandomPokemon
};

export default connect(mapStateToProps, mapDispatchToProps)(PokemonRandomizer);
  1. Use the injected prop with the Thunk in whatever place in the component you need to trigger the call.
// pokemon-randomizer/PokemonRandomizer.component.ts

const PokemonRandomizer = ({ findRandomPokemon }) => {
const onClickHandler = () => {
const randomNumber = getRandomNumber(MAX_POKEMONS);

findRandomPokemon(randomNumber);
};

// ...

return (
<RectangleButton text="Find new pokemon" onClick={onClickHandler} />;
)
};
  1. Inject the Reactive Data into every component you want to refresh when the information comes.
// pokemon-randomizer/PokemonRandomizer.container.ts

import { connect } from 'react-redux';
import { AppReducerState } from '../../App.reducers';
import { PokemonRandomizer } from './PokemonRandomizer.component';
import { selectPokemon } from './store/pokemon-randomizer.selectors';

const mapStateToProps = (state: AppReducerState) => ({
pokemon: selectPokemon(state)
});

const mapDispatchToProps = {};

export default connect(mapStateToProps, mapDispatchToProps)(PokemonRandomizer);
  1. Use the injected prop with the Reactive Data in whatever place in the components you need to trigger the call.
// pokemon-randomizer/PokemonRandomizer.component.ts

const PokemonRandomizer = ({ pokemon }) => {
// ...

return pokemon && <PokemonPresenter pokemon={pokemon} />;
};

Static Data (No Store)


Information that is only consumed by one component at a time.

This means it's information that does not need to trigger a change at the same time for multiple components and therefore, there's no need to save it in the Store.

If the information is consumed initially by one component, and later by another one, then we can save it within the Service itself to avoid multiple requests.

Implementation


  1. Create a function to make the API Call under the module's service.
// pokemon-randomizer/services/pokedex.service.ts

class PokedexService {
axiosInstance: AxiosInstance;

constructor(axios) {
this.axiosInstance = axios;
}

getPokemonAPIRequest(id: number): Promise<Pokemon> {
return this.axiosInstance
.get(`https://pokeapi.co/api/v2/pokemon/${id}`)
.then((response: { data: Pokemon }) => response.data);
}
}
  1. Create a variable and a setter function to save and set the information within the component's state through useState hook.
// pokemon-randomizer/PokemonRandomizer.component.ts

export const PokemonRandomizer = ({}): JSX.Element => {
const [pokemon, setPokemon] = useState();

//..
};
  1. Directly, call the service function from the component in whatever moment or place you want to trigger it. It can be in a useEffect or in a handler function.

3.1. useEffect Example

NOTE: Remember that useEffect functions, cannot be async, so you will need to create an async function within, and make the call synchronously. Or you can use the .then/.catch syntax if you want.

To understand why this happens read this article.

// pokemon-randomizer/PokemonRandomizer.component.ts

import { pokedexService } from './services/pokedex.service';

export const PokemonRandomizer = ({}): JSX.Element => {
const [pokemon, setPokemon] = useState();

useEffect(
function initPokemon() {
const fetchPokemon = async () => {
const randomNumber = getRandomNumber(MAX_POKEMONS);

try {
const pokemon = await pokedexService.getPokemonAPIRequest(randomNumber);

setPokemon(pokemon);
} catch (err) {
// catch error
}
};

fetchPokemon();
},
[]
);

//...

return pokemon && <PokemonPresenter pokemon={pokemon} />};
};

3.2. Handler Function Example

// pokemon-randomizer/PokemonRandomizer.component.ts

import { pokedexService } from './services/pokedex.service';

export const PokemonRandomizer = ({}): JSX.Element => {
const [pokemon, setPokemon] = useState();

const onClickHandler = async () => {
const randomNumber = getRandomNumber(MAX_POKEMONS);

try {
const pokemon = await pokedexService.getPokemonAPIRequest(randomNumber);

setPokemon(pokemon);
} catch (err) {
// catch error
}
};

//...

return (
<>
<RectangleButton text="Find new pokemon" onClick={onClickHandler} />;
{pokemon && <PokemonPresenter pokemon={pokemon} />};
</>
);
};