- Each section is linked to its corresponding commit, so you can click on its title to checkout the commit and have a better look of what"s going on.
- Render
<App/>
with react-18
:
const root = createRoot(document.getElementById("root"))
root.render(<App />)
- Use
@reach/dialog
to render a modal. Setting its state with enum
instead of the primitive boolean
to embrace SoC design (i.e. if you set its state with boolean
, you may get overlapped state from other components using that state).
- Abstracting away
event.target.value
into a reusable function by destructuring the input fields with event.currentTarget.elements
and then use .value
on them. Use TypeScript"s Pick
to create a new type from an extended interface.
- Typing components.
- Typing destructured props (lines 49 -> 51).
- Typing context (lines 14, 15, 24).
- A typical use case of
unknown
type: typing an utility function (lines 8 -> 12).
- Create a simple styled component with
emotion
(at 1:45).
- Use
React.cloneElement
to pass in a component as a prop.
- Enable (at 2:30)
emotion
"s inline styling for JSX elements (with its css
prop) by adding:
/** @jsx jsx */
import {jsx} from "@emotion/core"
import * as React from "react"
- Labelling generated CSS classNames from
emotion
by importing:
import styled from "@emotion/styled/macro"
import * as colors from "styles/colors"
import * as mq from "styles/media-queries"
- Make a styled component from a SVG icon by simply passing it as an argument to the
styled
method, e.g:
const Spinner = styled(FaSpinner)({/* styles here */})
- Use
keyframe
to define animations.
- The default
aria-label
attribute (at 2:20) is intended for use on interactive elements, or elements made to be interactive via other ARIA declarations, when there is no appropriate text visible in the DOM (i.e. for screen readers) that could be referenced as a label.
- It is highly recommended to treat types like functions (i.e. start with the small things first, then build up bigger things from those, rather than building a composed type right from the start).
- Use
[number]
to get an element"s type in an array, e.g:
interface BooksData {
books: {
id: string;
title: string;
author: string;
coverImageUrl: string;
pageCount: number;
publisher: string;
synopsis: string;
}[];
error?: {
message: string;
status: number;
};
}
type BookData = BooksData["books"][number];
- Use parentheses (at line 87) to type guard a "possibly null" object when accessing its properties.
- Usage of
event.target.elements
(at 1:20) and encodeURIComponent
(at 2:15).
- Control when to run
useEffect
"s callback (at 3:00).
- When to abstract a piece of functionality into a reusable module.
- A caveat of
window.fetch
is that it won"t reject your promise unless the network request is failed (at 0:43).
- Senior stuff/team work: using their abstractions to clean up your code.
- The user doesn"t want to submit their password every time they need to
make a request for data. They want to be able to log into the application and
then the application can continuously authenticate requests for them automatically. A common solution to this problem is to have a special limited use "token" which represents the user"s current session.
- In order to authenticate the user, this token (which can be anything that uniquely identifies the user, but a common standard is to use a JSON Web Token (JWT)) must be included with every request the client makes. That way the token can be invalidated (in the case that it"s lost or stolen) and the user doesn"t have to change their password. They simply re-authenticate to get a fresh token.
- The easiest way to manage displaying the right content to the user based on
whether they"ve logged in, is to split your app into two parts: Authenticated,
and Unauthenticated. Then you choose which to render based on whether you have
the user"s information.
- When the app loads in the first place, you"ll call your auth provider"s API
to retrieve a token if the user is already logged in. If they are, then you can
show a loading screen while you request the user"s data before rendering
anything else. If they aren"t, then you know you can render the login screen
right away.
- A common way to get the token is to use a third-party authentication services (or something similar provided by your back-end):
// Call some API to retrieve a token
const token = await authProvider.getToken()
// If there"s a token, then send it along with the requests you make
const headers = {
Authorization: token ? `Bearer ${token}` : undefined,
}
window.fetch("http://example.com/api", {headers})
- Make your types optional if you"re going to set a default for them (line 6-10 and line 132). Use
Partial
to set the type for ...props
in case you set that argument to be an empty object:
type Props = {
key_1: boolean,
key_2: Date
}
function Sample(param_1: string, { param_2, ...props }: {param_2?: number} & Partial<Props> = {}) { // If you didn"t set a default then it"s just ` & Props`
// Do something...
}
- How to merge params (at 1:30).
- It"s a good practice to extract
async
logics into an independent function then call it in inside the useEffect
rather than defining it from within (line 14-24 and 39).
- For better maintainability, it"s highly recommended to use early
return
s instead of one big return
with multiple ternary statements in it or all of the return
"s are conditional (line 62-85).
- How to handle 401 response.
- Build a
Promise
utility function which handles both POST and GET requests.
- Embracing Single-page App: you can change the URL when the user performs an action (like clicking a link or submitting a form). This all happens client-side and does not reload the browser by using
react-router
. Key takeaways: differences between BrowserRouter
, Routes
, Route
and Link
(at 6:50).
- How to handle URL redirects. Prioritize server-side redirects over client-side.
useMatch
to highlight the active nav item.
- Use template literal to perform
string
interpolation in case of building a forced string
type expression (at line 100).
- An app"s state can be separated into two types:
- UI state: Modal is open, item is highlighted, etc.
- Server cache: User data, tweets, contacts, etc.
We can drastically simplify our UI state management if we split out the server
cache into something separate.
const refetchBookSearchQuery = useRefetchBookSearchQuery(user) // This is a promise
// The "cleanup" function"s return type is `void`:
React.useEffect(() => {
// TS will catch this error, but JS allows it:
return () => refetchBookSearchQuery()
// So the right way to do this is to wrap the promise with an IIFE:
return () => (function cleanUp() {
refetchBookSearchQuery()
})()
}, [refetchBookSearchQuery])
- Explicitly type guard the
unknown
params by casting their type with the as
keyword (at line 17).
- Setting a default value to a variable if it"s
undefined
(at 1:00).
- Use nullish coalescing operator
??
with run-time array traversing methods (at line 25).
- Perform CRUD operations with
react-query
.
- If your
queryFn
depends on a variable, include it in your queryKey
array. How to optimize query keys.
- Invalidate query with
onSettled
option from the useMutation
hook (at 2:35).
- Clear the cache (e.g. when the user logs out or they make a
401
request) by using queryCache.clear()
.
- Refactor hooks from
react-query
into custom hooks to abstract implementation details and avoid the risk of syntax errors from duplicating the same piece of code over and over again (at 2:30).
- Set the
useErrorBoundary
option from the useMutation
hook to true
to get mutation errors to be thrown in the render phase and propagate to the nearest error boundary.
- Prefetch with
queryClient.prefetchQuery
and queryClient.removeQueries
.
- Persist cache with
useQuery
"s onSuccess
option using queryClient.setQueryData
.
- Perform optimistic updates (i.e. assuming the request is going to succeed and make
the UI appear as if it had) with
useMutation
"s onMutate
option (at 1:20). You can rollback optimistic updates in case of a mutation failure by using the onError
and onSettled
options (at 3:20).
- The common cases for
Context
(to eliminate prop-drilling) are application "toast" notifications, user authentication state, or modal and focus management.
- When to convert a function into a custom hook (also a notice on how hooks are used). A tip for when you have a function which needs to access the context"s value (at 02:18).
- The most practical use case of
useCallback
(at 1:20): when your function will be likely added to a dependency list.
- Improve maintainability by SoC: create a component as a wrapper, whose sole purpose is to manage and provide the context. A standard
context
file should look like this.
- It is recommmended to move all the contexts into a global context module (
index
file in the context
folder) for easier testing.
- Nested destructuring (at 0:25, it"s just another way of writing
useAuth().user.token
).
- Not all reusable components are actually reusable. Lots of the time what it turns into is a mess of props. Those end upbeing enormously difficult to use and maintain. They"re also riddled with performance problems and actual bugs.
- A recommended approach is to refactor your code by creating compound components which are structurally flexible (i.e. we don"t want to control the structure of the components), but we still want to have implicitly shared state between them. Utilizing
context
(at 07:45) will help us with that.
- Create a generic utility function (at 01:50) which calls many functions at once.
- Create a HOC (to embrace immutability) from a base component (at 01:45) to further customize an existing component.
- Performance optimizations are not free. They ALWAYS come with a cost but do NOT always come with a benefit to offset that cost. Therefore, optimize responsibly and make sure to measure your refactor to see if it"s worth it.
- Consider a suitable approach for your state management.
- Code splitting: no matter how big your application grows, it"s unlikely the user needs everything your application can do on the page at the same time. So if instead we split the application code and assets into logical "chunks" (using
React.lazy
) then we could load only the chunks necessary for what the user wants to do right then.
- Remember,
React.lazy
expects the module you"re importing to export a React component as the default export
.
- Use the Devtool"s
coverage
to measure if your optimization is worth it (at 4:50).
- If you"re using Webpack: prefetch a lazily loaded module with
/* webpackPrefetch: true */
(at 02:40):
const AuthenticatedApp = React.lazy(() =>
import(/* webpackPrefetch: true */ "./authenticated-app"),
)
- The only time the context"s provider re-renders is when the state actually changes (which is when you want consumers to re-render anyway). However, it"s often a good idea to memoize the functions we expose through context so those functions can be passed into dependency arrays. From there, we"ll memoize the context value as well.
- Measure and report your app"s performance with
React.Profiler
. How to customize the Profiler
(at 2:00) and how to use it (at 1:10).
- A remind (at 2:35) on where to put your
Profiler
. Add the Profiler
to your production build (at 1:10).
- There are three common approaches to data fetching:
- Fetch-on-render: We start rendering components and each of these components may trigger data fetching in their effects and lifecycle methods. A good example of that is fetching inside the
useEffect()
.
- Fetch-then-render: Start fetching all the data for the next screen first, then when the data is ready, render the new screen. We can’t render some of our components anything until the data arrives. The example of that is having a "Container" (or a "parent") component that handles data fetching and conditionally renders the child presentational component once we’ve received the data. In short, it"s not really an improvement from "fetch-on-render" but rather a different way to do it.
- Render-as-you-fetch: The idea here is that you, as a developer, most of the time, know what data your component needs (or there is a way to know it). So instead of waiting for the fetch to finish then do the render (or wait for the render to finish then fetch), we could render and fetch at the same time: Start fetching data as early as possible and start trying to render components that may still need data. As data streams in, React retries rendering components that still need data until they’re all ready. A practical use case: there are some assets we need to load before the app can render the initial page. The earlier we can start requesting those assets, the faster we can show the user their data. Start your requests for the needed code, data, or assets as soon as you have the information you need to retrieve them:
- Take a look at everything you"re loading.
- Determine the minimal amount of things you need to start rendering something useful to the user.
- Start loading those things as soon as you possibly can (i.e fetching data while your component is rendering, therefore reduce the "loading" time).
- The "customers" of the test are developers, so you want to make it as easy for them to understand as possible so they can work out what"s happening when a test fails.
- Various types of tests:
- Static: catch typos and type errors as you write the code.
- Unit: verify that individual, isolated parts works as expected (we"re often testing a single function).
- Integration: verify that several units works together in harmony (we"re normally testing a single screen).
- End to End (AKA "e2e test" or "functional testing"): a helper that behaves like an user to click around the app and verify that things function the way you want (we"re putting it all together and testing the application as a whole).
- Any piece of code that"s heavily relied upon is a good candidate for unit testing. Keep in mind that not everything needs a unit test. Think less about the code you are testing and more about the use cases that code supports.
- Test a pure function (at 1:00). Use
msw
to mock network request (at 1:00).
- How to know if you"re testing the right thing (at 1:10 and 2:35).
- Handle a promise"s error (line 89, 91 and 96 -> 108).
- Mock function call for testing with
jext.mock
and .toHaveBeenCalledTimes()
method (at 1:40).
- Automatically setup your server for testing if you"re using
create-react-app
.
- Use case of the
Symbol
primitive (at 2:10).
- When to refactor/abstract your tests.
- Typically, you"ll get confidence that your components are working when you run
them as part of integration tests with other components. However, highly
reusable or complex components/hooks can really benefit from a solid suite of
unit tests dedicated to them specifically. Sometimes this means that you"d mock some or
all of their dependencies and other times they don"t have any dependencies at
all.
- One thing to keep in mind, we want to try and use our code in the same way we
expect users to use them. So when testing the compound components, we should render them all together, rather than trying to render them separately from one
another.
- For testing components, we"ll be using
@testing-library/react
, the de-facto standard testing library for React, @testing-library/user-event
and @testing-library/jest-dom
to help with our user interactions. A contrived example of how to test a component with React Testing Library:
import * as React from "react"
import {render, screen} from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import {MyComponent} from "../my-component"
test("renders click me button", () => {
render(<MyComponent />)
const button = screen.getByRole("button", {name: /click me/i})
// `userEvent` returns promises for every API calls,
// so you"ll need to add `await` for any interaction you do.
await userEvent.click(button)
expect().toBeInTheDocument()
})
- Use
screen.debug
(at 1:10) to help with your test writing.
- A tip for
screen.getByRole
(at 1:40) to scope your queries down to a specific element.
- The easiest and most straightforward way to test a custom hook is to create a component that uses it and then test that component instead. To do that, we"ll make a test component that uses the hook in the typical way that our hook will be used and then test that component, therefore indirectly testing our hook.
- When it"s difficult to create a test component that resembles the typical way that our hook will be used, we can isolate the hook by creating a
null
component that only uses what the hook returns. Remember to wrap an act()
around your state update (at 1:30).
- Use
renderHook
from @testing-library/react-hooks
(at 1:00) to create tests for custom hooks. Here"s a quick example of how you"d test a custom hook with @testing-library/react
:
import {renderHook, act} from "@testing-library/react"
import useCounter from "../use-counter"
test("should increment counter", () => {
const {result} = renderHook(() => useCounter())
expect(result.current.count).toBe(0)
// All state updates/side effects should be wrapped in `act` to ensure that
// you"re testing the behavior the user would see in the browser.
act(() => {
result.current.increment()
})
expect(result.current.count).toBe(1)
})
- Remeber to wrap your state updates/side effects in an
act
(at 2:00) to ensure that you"re testing the behavior the user would see in the browser.
- Use
expect.any(Function)
(at 1:45) if you don"t care what a function returns in your test.
- Remember to
catch
a rejected promise (at 0:50).
- Use
jest.SpyOn()
and then expect(console.error).not.toHaveBeenCalled()
(at 0:40) to make your test fail (as expected) instead of passing and gives a console.error
warning. Use console.error.mockRestore()
to preserve the test"s isolation (at 2:20). Another workaround is to scope them for that specific expect error test.
- We should get most of our use case coverage from the Integration type tests. This is because they give us the most bang for our buck in regards to the level of confidence we can achieve relative to the amount of work they take. Write tests. Not too many. Mostly integration.
- With integration test, you"re typically only going to need to mock out HTTP requests and sometimes third party modules as well. You"ll find that you get a lot of confidence from getting the bulk of your coverage from integration tests and reserving unit tests for pure functions of complex logic and highly reusable code/components.
- Use the
wrapper
option of the render()
method from testing-library/react
(at 1:15) to test a component that uses context
.
- Reverse engineering to mock the logged in state (at 1:35).
- Use
window.history.pushState
(at 4:00) to update the testing route"s URL.
getByText
vs getByRole
(at 1:10) and getByRole
vs queryByRole
(at 3:50).
- For a myriad of reasons, you may find it preferable to not hit the actual backend during development. In such cases, use
msw
to create request handlers (regular HTTP calls as well as GraphQL queries) and return mock responses. It does this using a ServiceWorker, so you"ll see the fetch requests in the network tab, but as long as you have a mock handler, a real fetch call will not be made and instead your request handler can handle the request for you.
- How to test
Date
fields (at 3:50).
- Clean up your tests by separate repeated logics into modules. Sometimes when you"re testing a screen, you"ll notice you"re doing some repeated things that are specific to that screen and not generally applicable to other screens. When that happens, it could make sense to create abstractions for that screen.
- Test debounced input.
- When to use the
describe
block in your test (at 1:00).
- Test the expected error response from the server (at 3:20).
- There"s no better way to make automated tests resemble the way the user will use your software than to program an actual browser to interact with your application the same way a user would (without access to any internals, without
mocking the backend,...). This is called an "End-to-End" test (or E2E).
- For unit tests, we"re often testing a single function. For integration tests, we"re normally testing a single screen. For E2E tests, we"re putting it all together and testing the application as a whole. This means that typically the
E2E test follows a typical user flow which results in a longer, more comprehensive test that allows you to cover a lot of the most important use cases for your application.
- A well-known automated E2E testing tool is Cypress (another worthy alternative is Playwright if you want something new). How to configure Cypress.
- Notice when testing password inputs (at 1:45).
- The difference between
cy.findByRole()
and cy.findAllByRole()
(at 2:05).
- Force a "click" event (at 1:30).
- Avoid Common Testing Mistakes.