Skip to content

Latest commit

 

History

History
286 lines (263 loc) · 32.3 KB

WIL.md

File metadata and controls

286 lines (263 loc) · 32.3 KB

What I"ve learnt

  • 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 returns 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:
  1. UI state: Modal is open, item is highlighted, etc.
  2. 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.
  • Why use TypeScript:
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:
  1. 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().
  2. 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.
  3. 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:
    1. Take a look at everything you"re loading.
    2. Determine the minimal amount of things you need to start rendering something useful to the user.
    3. 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:
  1. Static: catch typos and type errors as you write the code.
  2. Unit: verify that individual, isolated parts works as expected (we"re often testing a single function).
  3. Integration: verify that several units works together in harmony (we"re normally testing a single screen).
  4. 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.