Skip to main content

Anti-Patterns

Introduction

The following is a list of common React Native anti-patterns that developers tend to fall into. The goal of this section is to identify those anti-patterns, explain why they can cause issues, and alternatives that allow developers to accomplish their intentions with best practices.

Passing objects or array literals as props to memoised components

When a component is declared using React.memo, React will perform a shallow equality check each render to compare the props of a component, and determine if it needs to be re-rendered. As a Javascript developer, most people will know that primitives are compared by value, meaning the following will evaluate to true:

const a = "string";
const b = "string";

a === b; // true

This means that a memoised component receiving primitive props, that haven't changed between renders, will not re-render:

const ParentComponent = () => {
return <MemoisedComponent firstProp="prop" secondProp="prop" />;
};

const MemoisedComponent = React.memo((props) => {
return (
<View>
<Text>
{props.firstProp} {props.secondProp}
</Text>
</View>
);
});

In this example React is performing a shallow equality check on firstProp and secondProp which will evaluate to true, skipping the re-render.

Where this can trip developers up is when passing objects or arrays to memoised components. Because Javascript compares objects (in JS arrays are considered objects) by reference, meaning two visually identical objects won't be considered equal.

const a = { value: "hello" };
const b = { value: "hello" };

a === b; // false

This is because a and b both have different addresses in memory, and JS will compare those addresses to determine equality. This means passing object and array literals to memoised components will defeat that memoisation because those props will be recreated each render, giving them a different memory address and in JS a different value to compare.

const ParentComponent = () => {
return (
<MemoisedComponent firstProp={[1, 2, 3]} secondProp={{ value: "hello" }} />
);
};

const MemoisedComponent = React.memo((props) => {
return (
<View>
<Text>
{props.firstProp} {props.secondProp}
</Text>
</View>
);
});

In this example React is comparing the previous and next value of firstProp and secondProp and deciding they are different, causing a re-render.

This means not only is the component still re-rendering for each render of the parent, we are wearing the cost of using React.memo without any of the benefits.

For this reason it's important to be conscious of what props you are passing to memoised components.

Solution

If you have decided that a component should be memoised, then any non-primitive values need to have a referentially stable memory address between renders. This can be achieved by either:

  • Declaring any constants or values not dependant on component state in the outer scope of the file. This method should be the first thing you reach for because it doesn't come with the overhead of useMemo. This will guarantee the value has the same reference between renders.
const firstProp = [1, 2, 3];
const secondProp = { value: "hello" };

const ParentComponent = () => {
return <MemoisedComponent firstProp={firstProp} secondProp={secondProp} />;
};
  • Declaring the value with the useMemo hook. It is important to properly set the dependecies, as failing to do so will mean the value returned by useMemo isn't as referentially stable as it should be which can defeat the component memoisation.

Passing arrow functions as props to memoised components

This anti-pattern isn't too different from the previous. In the following code snippet, the arrow function will be re-created each render, this means it will have a different reference each render and defeat the memoisation of the receiving component.

const ParentComponent = () => {
return <MemoisedComponent onPress={() => runFunction()} />;
};

When React goes to compare the new and old props it will think it needs to re-render because the identity of the onPress prop has changed.

Solution

  • Declare functions that don't require access to component state in the outer scope.
const onPress = () => console.log("PRESSED!");

const ParentComponent = () => {
return <MemoisedComponent onPress={onPress} />;
};
  • If a function requires access to any component state, wrap it in useCallback with the correct dependencies.
const ParentComponent = ({ id }) => {
const onPress = useCallback(() => {
console.log("PRESSED!", id);
}, [id]);

return <MemoisedComponent onPress={onPress} />;
};

Passing JSX as props to memoised components

Similar to the issues around passing object literals and inline functions, the result of re-creating JSX each render is a different reference which means a re-render will be triggered. This is because <Component /> === <Component /> will evaluate to false.

JSX produces an instance of React.createElement, which is an object. By passing JSX into a prop, that object is being recreated each render.

const Component = () => {
return <FlatList {...props} ListHeaderComponent={<ListHeaderComponent />} />;
};

Solution

Pass a React element to optimised components expecting a React element for a prop. A React element is an object that provides a description of the component instance that will be created. That element should be declared in the outer scope, or wrapped in useMemo, to ensure it is referentially stable.

/* Notice in both examples we aren't wrapping `ListHeaderComponent` in open/close tags */

const ListHeaderComponent = () => <View />;
const EntrantsList = () => {
return <FlatList {...props} ListHeaderComponent={ListHeaderComponent} />;
};

/* or if you need access to component state */

const EntrantsList = () => {
const ListHeaderComponent = useMemo(() => <View />, []);

return <FlatList {...props} ListHeaderComponent={ListHeaderComponent} />;
};

Using React.memo on components that receive JSX children

JSX tags are used to invoke React.createElement which returns an instance of a React element. This element instance will have a different identity each time it is created, meaning a component performing equality checks on them will fail.

export const Component1 = React.memo(({ children }) => {
return <View>{children}</View>;
});

const Component2 = () => {
return (
<Component1>
<Text>I am breaking memoisation!</Text>
</Component1>
);
};

In this instance memoising Component1 will not prevent re-renders because children will have a new identity each render.

This only applies to children that output React elements, not primitive values.

A component that just receives something like a string or number as children, such as a text component, can be memoised normally.

/* THIS IS FINE */
const ParentComponent = () => {
return <ChildComponent>I am a primitive child</ChildComponent>;
};

const ChildComponent = ({ children }) => {
return <Text>{children}</Text>;
};

Solution

Look at achieving optimisations in places other than component memoisation. This could mean memoising any components further down the tree that don't take children or optimising other props re-created each render. Address any issues that might be causing slow renders or whatever else suggested the component should be memoised.

Delayed state initialisation

A common pattern used for setting component state is through the use of the useEffect hook.

const Component = (props) => {
const availableExotics = useExotics(props.id);
const [selectedProductId, setSelectedProductId] = useState("");

useEffect(() => {
if (availableExotics[0]) {
setSelectedProductTypeId(availableExotics[0].product_type_id);
}
}, [availableExotics]);
};

In this example the value of selectedProductId changes when the value of availableExotics changes, but it could also change when another action calls setSelectedProductId such as a button press.

The issue is that when the component mounts and the render method is run for the first time the initial value of selectedProductId will trigger setState again. React will change the value of the state, and as a result the component will re-render.

Solution

This superfluous re-render could be skipped if the state was initialised with the correct value, which in this instance would be derived from availableExotics.

const Component = (props) => {
const availableExotics = useExotics(props.id);
const firstExotic = availableExotics[0];

const [selectedProductId, setSelectedProductId] = useState(
firstExotic?.product_type_id ?? ""
);

useEffect(() => {
if (firstExotic) {
setSelectedProductTypeId(firstExotic.product_type_id);
}
}, [firstExotic]);
};

Because the state setter has been invoked with the same value, React won't update the state value and trigger a re-render.

Unnecessarily controlling inputs

Especially when coming from React web, it's (rightfully) natural to implement a text input in this sort of way:

const ComponentWithInput = () => {
const [value, setValue] = useState("");

return <TextInput onChangeText={(text) => setValue(text)} value={value} />;
};

This is called a controlled component, meaning that the input's value is always driven by React's state. This approach can work in a lot of cases, but due to the inherent limitations of the React Native bridge it can cause jankiness and issues when rapidly inputting text, expecially on lower end devices.

  1. The native UI has to register a press.
  2. That event has to be sent over the bridge to JS.
  3. That event triggers a call to the onChangeText function .
  4. That runs the setState function.
  5. The state value is updated and the prop is updated.
  6. The change in the component state is sent back over the bridge to the native side.
  7. The native UI updates the value in the native component.
  8. Repeat.

As a result the bridge can become congested, and the user can experience less than optimal text inputs.

Solution

Where possible use a combination of a ref on the input and a defaultValue to manage inputs. This might look something like:

const ComponentWithInput = ({ submit }) => {
const inputRef = useRef<TextInput>();

const clearInput = () => {
inputRef?.current?.clear();
}

const submitForm = () => {
const value = inputRef?.current?.value;
submit(value);
}

return (
<TextInput ref={inputRef} defaultValue='' />
)
}
caution

This isn't always going to work as much of the time an input requies validation or masking. This is a known limitation of React Native and the devs are aware. It will hopefully be addressed with the new architecture.

Poorly managed or declared dependencies

When using React hooks it is important to properly consider the values inside each dependency array. We fortunately have a number of eslint rules setup to catch improper usage, but that rule can't catch everything, and you should still make sure you're verifying each dependency.

When declaring a dependency array be sure to consider the lifecycle of each value inside of it.

const ParentComponent = ({ IDs, addToBetslip }) => {
const raceEntrantIDs = IDs ?? [];

const onSubmit = (value) => {
// some logic
addToBetslip(value);
};

const onPress = useCallback(() => {
if (raceEntrantIDs.length > 5) {
onSubmit(raceEntrantIDs[0]);
}
}, [onSubmit, raceEntrantIDs]);

return <MemoisedComponent prop={raceEntrantIDs} onPress={onPress} />;
};

In the above example there are two main potential issues:

  1. IF IDs evaluates to falsy between renders MemoisedComponent will unnecessarily re-render despite receiving the empty array each time. This is because the re-render of ParentComponent will re-create that empty array.
  2. onPress is being re-created each render despite being wrapped in useCallback. This is because it has a dependency on onSubmit which will have a new reference each render, re-running the useCallback hook.

Solution

These could be solved by applying the tactics described in other similar anti-pattern solutions.

const EMPTY_ARRAY = [];

const ParentComponent = ({ IDs, onAddToBetslip }) => {
const raceEntrantIDs = IDs ?? EMPTY_ARRAY;

const onSubmit = useCallback((value) => {
// some logic
onAddToBetslip(value);
}, []);

const onPress = useCallback(() => {
if (raceEntrantIDs.length > 5) {
onSubmit(raceEntrantIDs[0]);
}
}, [onSubmit, raceEntrantIDs]);

return <MemoisedComponent prop={raceEntrantIDs} onPress={onPress} />;
};

Note: If you do find yourself having to implement this "cascading" memoisation, where you are wrapping function after function in useCallback just to satisfy this rule, it could be an indication that you should look at the way you have written your components. Here is an example:

const Component = () => {
// If addToBetslip is wrapped in useCallback this will need to be a dep and will also have to be wrapped in useCallback
const formatAmount = (amount) => {
if (amount > 100) {
return formatItThisWay(amount);
} else {
return formatItThatWay(amount);
}
};

// This needs to be added to the deps of handleButtonPress and wrapped in useCallback
const addToBetslip = (value) => {
const formattedStake = formatAmount(value);
dispatch(betslipActions.addToBetslip(formattedStake));
};

// This needs to be memoised because it's passed to a memoised component
const handleButtonPress = useCallback((value) => {
// some logic
addToBetslip(value);
}, []);

return <MemoisedComponent onPress={handleButtonPress} />;
};

Using "render" functions inside components

When needing to conditionally render components within JSX it's common to extract that render logic into a separate component scoped to the parent. This might look something like this:

const Component = ({ raceID, resulted }) => {
const renderOtherComponent = () => {
if (!raceID) return <Component1 />

if (resulted) return <Component2 />

return <Component3 />
}

return (
<SomeOutComponent>
{renderOtherComponent()}
</SomeOuterComponent>
)
}

The issue with this is that the renderOtherComponent function is unnecessarily recalculated each render, and new JSX is produced, even if the props have stayed the same.

Solution

This could be fixed in two ways:

  1. Wrapping the "render" function in useMemo:
const Component = ({ raceID, resulted }) => {
const ConditionalComponent = useMemo(() => {
if (!raceID) return <Component1 />;

if (resulted) return <Component2 />;

return <Component3 />;
}, [raceID, resulted]);

return <SomeOuterComponent>{ConditionalComponent}</SomeOuterComponent>;
};
  1. Creating a separate memoised component:
const ConditionalComponent = React.memo(({ raceID, resulted }) => {
if (!raceID) return <Component1 />;

if (resulted) return <Component2 />;

return <Component3 />;
});

const Component = () => {
return (
<SomeOuterComponent>
<ConditionalComponent raceID={raceID} resulted={resulted} />
</SomeOuterComponent>
);
};

Failing to memoise a value from the root to the end

When memoising a value to pass down as props it is important to maintain that memoisation until that value is utilised and reaches its "end". Failing to do so may mean components behave as if no memoisation is occurring, and in some scenarios can even cause a performance hit because of the overhead of memoising some values.

const Races = ({ raceIDs }) => {
const { isRaceAvailable } = useMadeUpAvailabilityHook();

const availableRaces = raceIDs.filter((id) => isRaceAvailable(id));

return <RaceCard availableRaces={availableRaces} />;
};

const RaceCard = ({ availableRaces }) => {
const races = useMemo(() => {
return availableRaces.map((race) => mappingLogic(race));
}, [availableRaces]);

return <MemoisedList races={races} />;
};

const MemoisedList = React.memo(({ races }) => {
return <FlatList {...props} data={races} />;
});

In the above example there is three elements. The first defines an array called availableRaces which is both the root of the value, and root of the issue. That array is being passed to RaceCard which is executing some mapping logic and correctly memoising the result before passing it to a memoised list component.

This sort of scenario is common and it's easy to overlook the issue, but because availableRaces inside Races isn't memoised, is being re-created each render, and isn't a primitive type meaning it has a different identity each render, it is defeating all down chain memoisation. The useMemo inside RaceCard will re-run every render even if the values inside of availableRaces haven't changed, which will cause React.memo to evaluate the old and new props as changed and causing a re-render of the memoised component.

Solution

Unfortunately there is no "do this" solution to this problem, so it's important to take a holistic look at your chain of components as well as measure, observe and address any re-renders and performance issues. If you do determine that a value or component needs to be memoised, make sure you check that each dependency or prop it receives will be as stable as possible from the point it is declared.