Are you a Junior React Dev? 4 ways to make your project more professional and impressive
If you want to:
- massively improve your developer experience,
- make your code more maintainable and easier for others to collaborate,
- impress hiring engineers with your GitHub projects,
then this post is for you!
1. Use Volta
Does any of the below sound too familiar to you?
- You clone some new repo, run npm install or yarn in it, and your terminal gets flooded with cryptic error messages to later find out in the readme that the app works only with Node versions 13.9.0 through 14.4.0.
- Your colleague casually adds “oh btw, you’ll need Node 12 to build it” at some point during your onboarding.
- When someone complains that they can’t get your project to run you always start by asking “So what version of Node and npm are you on right now?”.
- You concurrently work on multiple projects, each requiring different versions of some tooling.
Well, Volta solves all of these problems and does it exceptionally well. Here’s all you need to get started.
- Install it by running
curl https://get.volta.sh | bash
or, if you’re on Windows, follow the instructions from their website. - Run the
volta pin
command in your project’s root directory, to pin versions of specific tools to the package.json. For example, by running the following commands:
volta pin node@12.20
volta pin yarn@1.19
The following lines will be added to your package.json:
"volta": {
"node": "12.20.2",
"yarn": "1.19.2"
}
This way, when someone with Volta installed will run any commands that use Node or Yarn they will get the same versions automatically. If they don’t have them installed, Volta will install them and then run the command.
The thing about Volta is that it is extremely simple to use and once you start you’ll wonder how you managed to get by without it in the past.
2. Use Storybook
I can’t imagine working efficiently on anything larger than a Todo app without Storybook.
Really. I mean, I used to work on large projects without it, as most of us have, but since I started integrating Storybooks into my workflows I became much more productive. For example, I remember that to develop some small component I used to have to navigate the app to a certain screen for it to appear. If my changes made the app reload I had to look for the component all over again.
Some developers also notice that and they may create some kind of /playground
subpage with all pure components displayed for ease of development and visual testing. But that’s more or less what Storybook is, only much less powerful!
Storybook lets you write *.stories.tsx
files alongside your components and inside them, you can render your components with different props to display them in various states next to each other. Of course, since stories files import your components, all the changes you make to them get reflected inside Storybook in real time.
Using Storybook in your project encourages you to do component-driven development, that is, building UI by starting with basic, pure components and composing them into larger screens.
Pure components, like pure functions, have three very important properties:
- They are easier to test. Since pure components don’t perform any side effects, you can test them deterministically by just plugging different sets of props into them and asserting that they render what they should. No need for mocking or intercepting network requests.
- They are easier to reuse. Since pure components don’t rely on any hidden state, you should be able to stick them in different views inside your app or even reuse them across projects and they should just work.
- They are easier to compose. Instead of creating every screen, view, or layout by writing CSS for all the elements from scratch every time, you can just assemble your screen by simply putting your pure components inside a container and adjusting the layout by passing styling props specific to this composition. No need to create tons of “one-off” components like
TrendingPostsBottomButton
that cannot ever be reused because of their super-specific inner styling.
I only scratched the surface of the entire paradigm of the aforementioned component-driven development. If you want to level up as a UI engineer and also save yourself a lot of frustration that comes with poorly thought-out design systems I’d encourage you to check out these websites:
- https://www.componentdriven.org/
- https://bradfrost.com/blog/post/atomic-web-design/
- https://storybook.js.org/tutorials/intro-to-storybook/
3. Put business logic into custom hooks
Imagine we’re implementing a screen that should show the user all posts that are available on our blog. We did our homework from the previous point, so we have already written and tested in Storybook our pure components: PostsStast
and Post
. Now we only need a component that will fetch posts’ data and distribute it to its children.
Here’s something that we could come up with:
export const AllPosts = () => {
const [posts, setPosts] = useState<null | PostData[]>(null);
const [postsCount, setPostsCount] = useState(-1);
const [viewedPostsIds, setViewedPostsIds] = useState<number[]>([]);
const [isError, setIsError] = useState(false);
useEffect(() => {
fetch("http://localhost:1337/posts")
.then((res) => res.json())
.then((data) => setPosts(data))
.catch(() => setIsError(true));
}, []);
useEffect(() => {
fetch("http://localhost:1337/posts/count")
.then((res) => res.json())
.then((data) => setPostsCount(data))
.catch(() => setIsError(true));
}, []);
if (isError) {
return <div>Error...</div>;
}
if (postsCount === -1 || !posts) {
return <div>Loading...</div>;
}
const transformedPosts = posts.map((post) => ({
...post,
isViewed: viewedPostsIds.includes(post.id),
}));
return (
<div>
<h1>All Posts</h1>
<PostsStats postsCount={postsCount} />
{transformedPosts.map((post) => (
<Post
key={post.id}
post={post}
isViewed={post.isViewed}
onClick={() =>
setViewedPostsIds((posts) => {
if (posts.includes(post.id)) {
return posts;
}
return [...posts, post.id];
})
}
/>
))}
</div>
);
};
This component does… a lot of things.
First, it creates 4, separate but in some cases interdependent, pieces of state.
Then we fetch data, and since there are two endpoints we need to hit, we use two useEffects
.
Then we perform some checks to see if we should show an error or loading state.
And if the data loaded successfully, then we still need to combine the external data with our local state for tracking clicked articles.
Finally, we can display the screen with the correct data.
Now, this example isn’t too bad. But the thing is, by sticking to this way of coding you’re going to end up with components with 300+ lines of code even before the return
statement. Such components are hard to read, to maintain and are potentially full of race conditions between different state values and effects.
How to prevent this? Check this out:
export const AllPosts = () => {
const { posts, postsCount, isLoading, isError, view } = usePosts();
if (isLoading) {
return <div>Loading...</div>;
}
if (isError) {
return <div>Error...</div>;
}
return (
<div>
<h1>All Posts</h1>
<PostsStats postsCount={postsCount} />
{posts.map((post) => (
<Post
key={post.id}
post={post}
isViewed={post.isViewed}
onClick={() => view(post.id)}
/>
))}
</div>
);
};
This approach has a few advantages:
- Everyone can see at a glance what this component is supposed to do.
- We can refactor, improve and test our custom
usePosts
without modifying this or any other component consuming it as long as we don’t change its API. - Thanks to the
isLoading
andisError
flags we can easily determine what UI we should render. - A new business requirement comes and we have to display posts in another place in our app? No problem! We have all the logic nicely contained inside the
usePosts
hook. - Custom hooks are also great to compose. Do we need to show just viewed posts? Simply create
useViewedPosts
that callsusePosts
under the hood and returns filtered results.
Check out React’s official (beta) docs for a comprehensive guide to writing custom hooks.
4. Write simple code
This last one is arguably the most important, but also the hardest one to master.
Say, you need to fetch some data and show it to a user as a table. You don’t need to create some boilerplate-y, obscure, object-oriented abstraction to handle the fetching and then add yet another abstraction layer to use it in your functional React component. Instead, you could just use a react-query hook to fetch your data and map it to a table inside your component. Everything is inside a single component, with no obscure abstractions or indirections.
Simple code is a combination of choosing accurate variable names, designing sensible APIs for your functions, writing clean code, and choosing good tools for the job.
How to improve in this area:
- Write a lot of code. Rewrite the same thing using different libraries, patterns, or architectures. This way you’ll build knowledge of what approaches work the best for which problems.
- Take time to read the code and analyze if the snippet, function, or even the whole feature could’ve been implemented more simply.
- Keep up (at least to some extent) with programming communities where people exchange their knowledge and share libraries and techniques you might’ve not known. Some examples are programming subreddits such as r/react, and r/typescript; following people like Matt Pocock, Josh W. Comeau, or me :)
Thanks for reading 😊. Did you find one of these tips particularly helpful? Or maybe you have something to add? Feel free to comment, write us an email, or reach out on Twitter.
Reviewed by: Krzysztof Grajek