We often get asked to render a list in a React interview. In this article we are going to look at a basic implementation and come up with four ways we can improve it to stand out from the rest.
Standard Implementation
Let's look at a basic implementation where we render a list based on an array of items. Try to think of at least three different ways we can improve the following implementation before reading further.
import { useState, useEffect } from "react";
const App = () => {
const [posts, setPosts] = useState([]);
const [currentPost, setCurrentPost] = useState(undefined);
useEffect(() => {
const initialize = async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/posts");
const json = await res.json();
setPosts(json);
};
initialize();
}, []);
const onPostClick = (post) => {
setCurrentPost(post);
};
return (
<div>
{currentPost && <h1>{currentPost.title}</h1>}
<PostList posts={posts} onPostClick={onPostClick} />
</div>
);
};
const PostList = ({ posts, onPostClick }) => {
return (
<div>
{posts.map((post) => (
<Post post={post} onPostClick={onPostClick} />
))}
</div>
);
};
const Post = ({ post, onPostClick }) => {
const onClick = () => {
onPostClick(post);
};
return <div onClick={onClick}>{post.title}</div>;
};
export default App;
Improvements
Here are the four improvements that we can make to stand out. It’s important we articulate the why during the course of the interview.
1. Specify Key
By providing a key
prop to our list item components, we help React identify each item when it compares the original tree with its subsequent tree. It’s important to emphasize that the key
prop needs to be unique and we shouldn’t use an index as a key
(changing the order on the list doesn’t change the identity of the item).
{posts.map((post) => (
<Post key={post.id} post={post} onPostClick={onPostClick} />
))}
2. Optimize Rendering
Every time we click on a list item, we are re-rendering PostList
and every Post
.
const Post = ({ post, onPostClick }) => {
console.log("post rendered");
const onClick = () => {
onPostClick(post);
};
return <div onClick={onClick}>{post}</div>;
};
We can optimize our PostList
component by using the memo
function provided by React. When we wrap our component with memo
, we are telling React to not re-render this component unless the props have changed.
import { useState, useEffect, memo } from "react";
const PostList = memo(({ posts, onPostClick }) => {
return (
<div>
{posts.map((post) => (
<Post post={post} onPostClick={onPostClick} />
))}
</div>
);
});
However, we will notice that our component continues to re-render even with memo
. Our App
re-renders every time currentPost
state changes. Every re-render it is re-creating the onPostClick
function. When a function is re-created (even if it’s the same implementation), it has a new identity. Therefore, the props technically did change, which means PostList
will re-render.
const fn1 = () => {};
const fn2 = () => {};
fn1 === fn2; // => false
We can tell React to not re-create the function by using the useCallback
hook.
const onPostClick = useCallback((post) => {
setCurrentPost(post);
}, []);
Using useCallback
might have made sense in the previous example because it is preventing us from re-rendering all of the posts again. It’s important to point out that it doesn’t always make sense to wrap a function in a useCallback
.
const Post = ({ post, onPostClick }) => {
const useCalllback(onClick = () => {
onPostClick(post);
}, []);
return <div onClick={onClick}>{post.title}</div>;
};
We can point out to the interviewer that using the useCallback
in the Post
component might not make sense because the component in this case is lightweight. We should only use useCallback
if it makes sense (we can test by profiling). There are downsides to useCallback
; it increases the complexity of the code and calling useCallback
is additional code that gets run on every render.
3. Clean up when component un-mounts
Right now we are not doing any sort of clean up when the component un-mounts. For example, what if we decide to navigate away from the page before we get a response from our URL? We should cancel the request.
useEffect(() => {
const initialize = async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/posts");
const json = await res.json();
setPosts(json);
};
initialize();
}, []);
useEffect
can be split up into two parts: code to run on mount and code to run on unmount:
useEffect(() => {
// When component mounts what code should I run?
return () => {
// When component unmounts what code should I run (clean up)?
};
}, []);
We can cancel the request by using the AbortController
and calling controller.abort()
on clean up.
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
const initialize = async () => {
try {
const res = await fetch("https://jsonplaceholder.typicode.com/posts", { signal });
const json = await res.json();
setPosts(json);
} catch (err) {
console.log(err);
}
};
initialize();
return () => {
controller.abort();
};
}, []);
4. Add accessibility
Final test that truly separates an exceptional candidate is if the candidate can talk about accessibility. Our sample application is too simple to add any tangible accessibility improvements, we should definitely talk about some things to look out for once our application grows in complexity. One test we can run is, can we use our sample application using the keyboard alone? One quick fix would be to convert items into buttons so that we can tab through them using our keyboard.
const Post = ({ post, onPostClick }) => {
const onClick = () => {
onPostClick(post);
};
return <button onClick={onClick}>{post}</button>;
};
Conclusion
Rendering a list in React seems like a simple interview question at first. Sometimes candidates might get frustrated why they didn’t pass the interview despite being able to implement a working solution. Next time we encounter this question, make sure to communicate to the interviewer (and implement them if given the time) the different ways we can render a list like a pro.
Top comments (14)
Nice article!
One suggestion for 1. Specify key: Sometimes your data structure doesn’t contain a usable value necessary for the
key
. In those cases we can use the second.map()
parameter (index
) to generate our keys on the fly like so:Edit: You should only use the array index for your keys if your array is static and there is no requirement for filtering nor sorting. Examples could be your primary navigation menu, or a list of tags associated with an article, or even a list of posts.
Source: developer.mozilla.org/en-US/docs/W...
The index of an element should not be used as a key, as the order could change.
Here is some more info on the subject:
React official docs
robinpokorny.com/blog/index-as-a-k...
Absolutely agree—if you are intending to manipulate or update the array (by adding, removing or re-ordering the items) then a generated key (or if available, an array item prop) is definitely the way to go.
But... if the array is static and never to be filtered nor sorted, then using the array index is perfectly fine—"Horses for courses" 🏇
There was no mention in the OP that array manipulation was an expectation, which was the basis for my suggestion. I'll edit my original comment for clarification.
Never thought of it that way, excellent reasoning!
It's only partially true
Don't use the ID as the key if the order might change, but there are a bunch of cases where the order of the elements won't change, in which case you can use the ID as the key (unless you have no other options)
The documentation says it's not recommended, but it's not forbidden either to use ID as key, In documentation you can even read that
Never use useMemo or any memorization at first place. useMemo() basically compares new changes with old changes, and each comparison takes time than normal program flow. Use it only when it's necessary
Yes, we should only memoize when it's necessary. In this article we memoized the component that was re-rendering 100 posts every time the the parent state changed but we chose not to memoize inside of the item component. Adding memoization adds code complexity and additional code execution...sometimes these costs don't outweigh the benefits. There might be ways to avoid re-rendering by restructuring the app as well: Before You memo().
I love that article - "Before You memo()" !
That gave me way more insight than the ubiquitous "just use memo or callback" 'hack' which gets thrown around everywhere you look. Sprinkling 'use memo' all over your code just doesn't feel right.
But I stand by my opinion that this sort of "low level" mechanical thing should be taken care of by the framework rather than the programmer (hence why I'm still more impressed by Vue than by React).
I don't know, this is what still bugs me about React, compared to Vue - Vue is more like - it lets you code the logic, and tries to optimize the reactivity and the rendering itself, while with React the burden is more on the programmer (as your "useMemo" and "useCallback" examples show).
Vue just does more for you, and on top of that it's even smaller, and more performant ... but yeah React has the jobs and the popularity :-P
Considering point 4 is about accessibility, I'm surprised you chose to use buttons inside a div instead of an ul with lis, coupled with tabIndexes or something in that spirit. It would've been way more semantic, considering we are rendering a list.
Some time Post onClick is costlier if no of post grows. So use event delegation technique so that both onClick and useCallback can be removed.
Nice tips
This is a great article! I am a junior React developer and I am preparing for interviews and this will be of great help. Thank you!
Hallo can we like each other's posts?