Graduate Program KB

History

  • jQuery

    • Designed to simplify DOM traversal and update the state of your application
    • Shared mutable state within the DOM became an issue, hard to track and predict variables
  • Backbone

    • Uses Model, View, Controller software design pattern
    • State is stored in the models, rather than the DOM
    • Views that cared about a model's state would re-render
  • Angular

    • Full-featured framework, it does a lot of things
      • Data binding

        • Two-way binding, update view when model changed, and updated the model when view changed
        • Removes manual DOM manipulation, but implicit changes makes code hard to follow
        • Performance issues since Angular had to constantly scan an application looking for state changes
      • Routing, dependancy injection, directives, data filters, templates

  • React

    • View is a function of the application state view = function(state)

      • Changes in state automatically update the DOM
    • No more explicit DOM operations, everything is declarative

      • Updating the DOM is now simple and predictable by moving state into reusable components
    • Separation of concerns principle: separating a computer program into distinct components

      • Typically, HTML, CSS and JS were all separated
      • React has a different interpretation, any technology which rendered the view (HTML, JS and sometimes styling, CSS) were part of the same concern
      • HTML and JS is combined in JSX
    • From 2014-2020, React was used with React Router to primarily create single-page applications

    • Nowadays, React acts as a UI primitive, it's built on top with other frameworks such as Next.js, Remix and Astro which handle server side rendering and route pre-fetching, etc.

Tips

  • Capitalisation of component names help React distinguish custom elements from the ones native to the DOM

  • If you want to render nothing while some data is still loading as an example, return null from the component

  • In components, only a single top-level element can be returned

    • Don't wrap with div
    return (
        <div>
            ...
        </div>
    )
    
    • Use the following instead:
    return (
        <React.Fragment>
            ...
        </React.Fragment>
    )
    
    // Or use the shorthand
    return (
        <>
            ...
        </>
    )
    

Props

  • Props are to components what arguments are to functions

  • Data that is not a string must be wrapped in { }

  • Props are stored on an object and passed as the first argument to the function

    • Each prop is added as a key on the object by setting attributes
    • Can pass anything to props, even other components
    • Props that don't have an initial value are set to true
    function Greet(props) {
        return <h1>Greetings, {props.firstname} {props.lastname}</h1>
    
        // props = {
        //    firstname: 'John',
        //    lastname: 'Smith',
        //    age: true
        // }
    }
    
    export default function App() {
        return <Greet firstname='John' lastname='Smith' age/>
    }
    
  • Data passed to a component through the body instead of setting attributes are stored under the children property of the props object

    function Greet(props) {
        return <div>{props.children}</div>
    }
    
    export default function App() {
        return <Layout>Hello, World!</Layout>
    }
    

Elements and Components

  • Elements are object representations of a DOM node, they are the building blocks of React

    • Allows current and previous rendered element to easily be compared and updated
    • Parameters: Tag string, object of attributes
    const element = jsx('h1', {
        className: 'header',
        children: 'Profile'
    })
    
  • Not limited to using native HTML elements, can utilise other React components as well

    • Components optionally take in a parameter via props, and return an element
    • If the first argument to jsx is a component, React recursively renders it and the children components to retrieve all the elements and get the final description of the UI
    function Profile({ description }) {
        const element = jsx(User, {
            name: 'John',
            age: 20,
            children: { description }
        })
    
        return element
    }
    

Event listeners

  • Pass functions to listeners as a reference, not as an invocation
  • Create functions within the component so they have access to props
  • Some common event listeners:
    • Keyboard: onKeyDown, onKeyPress, onKeyUp
    • Form: onChange, onInput, onInvalid, onReset, onSubmit
    • Generic: onError, onLoad
    • Mouse: onClick, onContextMenu, onDoubleClick, onDrag, onMouseDown, onMouseUp, onMouseMove, onMouseOut
    • Selection: onSelect
    • UI: onScroll
    • Wheel: onWheel
    • Other: onToggle

useState and Rendering

  • useState is a hook for managing states that persist and triggers re-rendering of components

    • Takes in one argument, the initial value of the state
    • Returns an array containing the state and a function to update that state
    const [count, setCount] = useState(0)
    const [friendsList, setFriendsList] = useState([])
    const [mode, setMode] = useState('light')
    
  • Components render only if a state updates

    • React snapshots the current state of the component on the initial render and subsequent re-renders to determine if future changes occur
    • React uses an algorithm called "batching", when multiple invocations of a state setter function occurs, it tracks all of them but only uses the result of the last invocation for each state
    • Note that in setCount, the value of count are both equal based off the previous captured snapshot. It doesn't set count first, then use that updated value in the next set count, which would seem intuitive to beginners
    const [count, setCount] = useState(0)
    const handler = () => {
        setCount(count + 1) // Tracks state change
        setCount(count + 1) // State here is the same in previous update function (+0), but uses this result since it overrides the first call
    }
    
    const handlerTwo = () => {
        setCount(count + 1) // Tracks state change
        setCount(count + 9) // State changes from the previous update (+8), use last call
    }
    
    • It's possible to use the value of the previous invocation of the updater function instead of replacing it. Just pass a function with an argument that takes in the result of the previous call
    • Can use a callback function if the application relies on the previous state, it's guaranteed to return the most recent state
    • For example, an incremental counter relies on the previous state, but a state storing the selected colour theme has no relationship to the previous state
    const [count, setCount] = useState(0)
    const handler = () => {
        setCount((prevCount) => prevCount + 1)      // firstCount = prevCount + 1
        setCOunt((prevCount) => prevCount + 1)      // count = firstCount + 1 = (prevCount + 1) + 1 = prevCount + 2, THIS WORKS!
    }
    
    const [theme, setTheme] = useState('Light')
    const themes = ['Light', 'Dark', 'Gray', 'Ocean']
    const handler = (buttonID) => {
        setTheme(themes[buttonID])      // Based off button clicked, it updates the theme
    }
    
    return <> ... </>       // Renders 4 buttons corresponding to a colour theme
    
    • An event handler only re-renders at most once, even if there are multiple state updates. Due to batching, state updates are enqueued after each call but the re-render only occurs after all the state updates are processed
    • There are two different setter calls below, both states update then only after the setExponential call, the component re-renders
    const [linear, setLinear] = useState(0)
    const [exponential, setExponential] = useState(1)
    const handler = () => {
        setLinear(linear + 1)
        setExponential(exponential * 2)
    }
    
    const handlerTwo = () => {
        setLinear(linear + 1)               // Tracks state of linear
        setExponential(exponential + 1)     // Tracks state of exponential
        setLinear(linear + 2)               // State changes from the previous linear, use last call
        setExponential(exponential * 3)     // State changes from the previous exponential, use last call
    }
    
    • Exception: Components re-render despite no state changes if they are children to another component. If the parent component's state changes, a re-render will occur and will also recursively re-render the child component as a result

useEffect

  • Rule #0: When a component renders, it should do so without running into any side effects

  • Rule #1: If a side effect is triggered by an event, put that side effect in an event handler

  • Rule #2: If a side effect is synchronising your component with some external system, put that side effect in useEffect

  • In functions, side effects are unexpected behaviours which occur anytime the function does anything other than take some input and return some output

    • Specifically, the output of a function should be the same for the given input anytime the function runs
    • In React, components take some input, in this case, the props and state, and the output is a View
    • Side effects can include API calls, manual DOM manipulation, browser APIs like localStorage and setTimeout
  • useEffect is a hook that allows you to run a side effect

    • The goal of useEffect is to synchronise a component with the external system, whenever any dependencies change, resynchronisation occurs
    • Dependencies are typically the state or props which can be passed into useEffect as a dependency array
    • Not passing a dependency array will run the side effect after everytime the component re-renders
    • Passing in an empty array will only run the side effect once, after the initial render
    • An optional cleanup function can be returned from useEffect that invokes before components re-render or unmount, can unallocate memory to data streams amongst other cleanup tasks
    • The side effect is delayed until a component is rendered to allow rendering to be as fast as possible
  • Consider you want to save the theme of an application so it appears the same even if you closed the browser

    • You can use localStorage to store the last state and retrieve it later on
    • However, it will render light mode first then the previous theme after
    function Theme() {
        const [theme, setTheme] = useState('Light')
        const themes = ['Light', 'Dark', 'Gray', 'Ocean']
        const handler = (buttonID) => {
            setTheme(themes[buttonID])  
            localStorage.setItem('theme', themes[buttonID])     // Store the latest theme in localStorage on click, rule #1
        }
    
        React.useEffect(() => {     // Rule #0, render first, run side effect after
            const newTheme = localStorage.getItem("theme")
    
            if (newTheme) {
                setTheme(newTheme)
            }
        }, [])  // This side effect is ran only after the initial render to retrieve the previous theme before the browser was closed, rule #2
    
        return <> ... </>       // Renders 4 buttons corresponding to a colour theme
    }
    
  • Can use lazy state initialisation to pass a function to useState which will be invoked once on the initial render

    • This method can disrupt the flow of rendering by running the side effect at the same time but removes the double render at the start
    function Theme() {
        const [theme, setTheme] = useState(() => {
            const oldTheme = localStorage.getItem('theme')      // Violates rule #0 and #2
            return oldTheme ? oldTheme : 0
        })
    
        const themes = ['Light', 'Dark', 'Gray', 'Ocean']
        const handler = (buttonID) => {
            setTheme(themes[buttonID])  
            localStorage.setItem('theme', themes[buttonID])     // Store the latest theme in localStorage, rule #1
        }
    
        // useEffect is gone, can also set localStorage in useEffect based off the state dependency but it violates rule #2 of using an event handler
    
        return <> ... </>       // Renders 4 buttons corresponding to a colour theme
    }
    
  • Adding event listeners:

    • useEffect is invoked once by passing an updater function to the state setter method rather than using a closure
    • No longer adds and removes an event listener multiple times due to the state invoking the side effect multiple times
    • The handleChange function registered its reference to the event listener so whenever an event occurs, handleChange is invoked to update state
    useEffect(() => {
        const handleChange = () => {
            if (document.visibilityState !== 'visible') {
                setCount((c) => c + 1)      // From setCount(count + 1)
            }
        }
    
        document.addEventListener('visibilitychange', handleChange)
    
        return () => {
            document.removeEventListener('visibilitychange', handleChange)
        }
    }, [])      //From [count]
    
  • Rules for some other cases:

    • Rule #3: If a side effect is synchronising your component and the side effect needs to run before rendering, put that side effect inside useLayoutEffect

      • Will help with the double render problem in the first themes example
      • useLayoutEffect API can be used in the same way as useEffect, except useEffect runs asynchronously after render whereas here, useLayoutEffect runs synchronously before anything is shown to the user
      • Use if the layout information needs to be synchronised with the component
    • Rule #4: If a side effect is subscribing to an external store, use the useSyncExternalStore hook

      • If non-React state is already managed by an external system, such as another library, use useSyncExternalStore to subscribe a component to that state without needing to duplicate it as a React state
      • It imports a function that subscribes to the external state so it knows when to change, and another function which reads the external state called a snapshot
      • In the following code, whenever a new online or offline event occurs, the callback function (getSnapshot) will be invoked to re-render the component with the latest value
      const getSnapshot = () => {
          return navigator.onLine ? "online" : "offline"
      }
      
      const subscribe = () => {
          window.addEventListener("online", callback)
          window.addEventListener("offline", callback)
      
          return () => {
              window.removeEventListener("online", callback)
              window.removeEventListener("offline", callback)
          }
      }
      
      React.useSyncExternalStore(subscribe, getSnapshot)
      
      • Usually, the subscribe and snapshot functions are declared outside the component, otherwise if renders occur, it will unsubscribe then resubscribe everytime
      • If they need to be in the component, use the hooks useMemo or useCallback to maintain a stable reference

useRef

  • useRef hook creates a value that's preserved across renders, but won't trigger a re-render when it changes, unlike states
    • Useful for changing values that don't affect the View but need to be stored for later
    • Takes in an initial value of the argument and returns an object with the current property
    • The object is directly mutable
    const ref = useRef(null)
    ref.current = 10
    /*
        {
            current: 10
        }
     */
    
    
  • Passing references as a prop to a react element will put a reference to the DOM node it creates in that ref's current property

useContext

  • Moving props between components can become inefficient and difficult to manage in very large applications

  • React has a built-in API called Context which gives data the ability to 'teleport' to anywhere in the component tree

    • Context is used to simply move data within the component tree, not manage state
      1. Create a Context
    const context = React.createContext()
    
    export default context
    
      1. createContext provides an object with the Provider property
    • Any component within the subtree can receive access to the value prop
    import context from './context'
    
    export default function App() {
        const theme = 'Light'
    
        return (
            <context.Provider value={theme}>
                <Component1 />
                <Component2 />
                <Component3 />
            </context.Provider>
        )
    }
    
      1. useContext hook provides access to the value prop from the Context
    • Whenever useContext is invoked, it'll return whatever you passed to the value prop of the nearest Provider component of the same Context
    • If there is no Provider component above it in the tree, the value will be the first argument when createContext created the Context object
    import context from './context'
    
    export default function Component1() {
        const theme = React.useContext(context)
    
        return (
            <h1>{theme}</h1>
        )
    }
    

useReducer

  • Reducer pattern: Functional programming pattern that takes a collection as input and returns a single value as output

    • Keeps functions pure, can track state within a reducer function rather relying on an external variable
  • For React, consider a series of actions that can all be managed in one place

    • useReducer hook can be used to manage state using the reducer pattern
    • Imports a reducer function and initial state value then outputs the state and dispatch function
    • dispatch takes in one argument which should have a property to specify the action, and any additional information which may be used to update state
    • The reducer function takes in the current state by default and the argument passed by dispatch as the second argument, action
    • State updates depend on the type of action which is determined with a chain of predicates, within each scope is a unique implementation to update state
    initialValue = 0
    
    function reducer(state, action) {
        if (action.type == 'action1') {
            ...
        }
        else if (action.type == 'action2') {
            ...
        }
    
        return newState
    }
    
    export default function App() {
        const [state, dispatch] = React.useReducer(reducer, initialValue)
        const handleClick = () => {
            dispatch({  type: 'action', ... })
        }
    }
    
  • Fundamentally, useReducer and useState achieve the same things but there are some advantages:

    • More declarative by specifying what types of actions taken and map them to state transitions, rather than having lots of state setter invocations

    • Useful for states that update together or if a state update is dependent on other state

    • Decouples how the state is updated from the action that triggered the update

      • Can trigger a state change within useEffect without needing to include state properties as dependencies
      • The code increments a counter based on the step variable. Naturally, count and step should be in the dependency array but this would continually re-render and recreate the interval
      • To counterract this, setCount takes in a function to use the previous state, removing count from the dependencies, but step is still there and the same issue arises
      React.useEffect(() => {
          const id = window.setInterval(() => {
              setCount((count) => count + step)
          }, 1000)
      
          return () => window.clearInterval(id)
      }, [step])
      
      • Now, due to the nature of decoupling when using useReducer, the side effect is run only once, regardless of the counter and step changing
      React.useEffect(() => {
          const id = window.setInterval(() => {
              dispatch({ type: 'increment' })
          }, 1000)
      
          return () => window.clearInterval(id)
      }, [])
      

Memoisation

  • React re-renders all child components regardless of whether they accept any props

    • To overrride this default behaviour, use React.memo to pass in a component which re-renders only when its props change
    function App() { return <> ... </> }
    export default React.memo(App)
    
    // or
    
    function App() { return <> ... </> }
    const memoizedComponent = React.memo(App)
    
    • It knows when to re-render child components by performing a shallow comparison of the old and new props
    • Despite the properties of the prop object being the same, re-renders technically create and pass brand new objects, which means React.memo becomes redundant
  • useMemo allows you to cache the results of calculations between renders

    • It imports a function which returns the value you want to cache, and a second argument of dependencies which determines if the cached value is recalculated
    function Parent() {
        const handleClick = React.useMemo(() => {
            return () => {}
        }, [ ... ])
    
        const options = React.useMemo(() => {
            return {
                theme: 'Light',
                ...
            }
        }, [ ... ])
    
        return (
            <Child onClick={handleClick} options={options} />
        )
    }
    
    function Child({ onClick, options }) {
        return <> ... </>
    }
    
  • useCallback returns a memoised callback, basically same usage as useMemo but for functions. It takes in the same parameters as well

Custom Hooks

  • Custom hooks are functions that are prefixed with use and calls other hooks inside of it

    • It must use at least one of the built-in hooks, otherwise it's just a normal JavaScript function
    • The prefix makes it clear where hooks are being utilised, and it identifies aspects that use React features
  • Custom hooks enable the composition and reusability of non-visual logic

    • If there are multiple components which require the same logic, such as checking for network status, getting themes from locale, etc.
    • Create hook in another file and export it
    const getSnapshot = () => {
        return navigator.onLine ? "online" : "offline"
    }
    
    const subscribe = (callback) => {
        window.addEventListener("online", callback);
        window.addEventListener("offline", callback);
    
        return () => {
            window.removeEventListener("online", callback);
            window.removeEventListener("offline", callback);
        }
    }
    
    export default function useNetworkStatus () {
        return React.useSyncExternalStore(subscribe, getSnapshot));
    }
    

Legacy React

Components

  • Components are class-based and extend React.Component to gain access to specific features and methods

    • State and data: this.state, this.props
    • Updating state: this.setState
    • Lifecycle methods: componentDidMount, componentDidUpdate, componentWillUnmount
    • Obtain description of UI: render
  • Methods must be binded to this so the method knows what context to reference no matter where the function is invoked

    class Component extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                count: 0;
            }
    
            this.increment = this.increment.bind(this);
        }
    
        const increment = () => {
            this.setState(({ count }) => {
                return { count: count + 1 };
            });
        }
    
        render() {
            <>
                <h1>{count}</h1>
                <button onClick={this.increment}>Increment</button>
            </>
        }   
    }
    

Props

  • Passing data to a component

    • Setting attributes to a React component passes that data as a prop, if no data is specified then the initial value for that attribute is set to true
    • The attributes are set to this.props as a key
    <Component name='Owen' surname='Ten' />
    
  • Accessing props

    • Use the props key on the component's instance of this
    class Component extends React.Component {
        render() {
            return <h1>{this.props.name} {this.props.surname}</h1>;
        }
    }
    
  • Before hooks were introduced, components were only created using classes. But, functional components were used in cases where there was no state logic, only if props were passed to it.

  • Functional components in this case, were only concerned with providing a description of the UI based off props passed to it

    function NavBar({ names, selectName }) {
        const nameOptions = names.map((name) => <option key={name} value={name}>name</option> );
    
        render() {
            <>
                <select onChange={(e) => selectName(e.target.value)}>
                    {nameOptions}
                </select>
            </>
        }
    }
    
  • PropTypes enable data passed to props to be of a type of property

  • If types don't match what is specified, then a warning will appear

    import PropTypes from "prop-types";
    
    function StatelessComponent({ name, age }) { ... }
    
    StatelessComponent.propTypes = {
        name: PropTypes.string.isRequired,
        age: PropTypes.number.isRequired
    };
    

Lifecycle

  • Components manage their own state, receive data via props and describe their UI

  • Other responsibilities... AJAX requests, managing listeners, reacting to new props...

  • Mounting: Component gets added to the DOM

    • Initial state: Component state is set by using the constructor
    • Render: Through the render function, describe the type of DOM node to render based off its state and props
    • AJAX request and setting up listeners can be set up in componentDidMount which is invoked once when first mounted
    componentDidMount() {
        fetch(...);
        //Add listeners
    }
    
  • Updating: Component updates its state or receives new data via props

    • Re-render UI
    • Re-render data
    • Reset a listener
    • componentDidUpdate is invoked after the state changes or it receives new props but not on the initial render
    • Comparisons between state and props can be done to determine if any changes are needed
    componentDidUpdate(prevProps, prevState) {
        this.setState(...);
        fetch(...);
    }
    
  • Unmounting: Component gets removed from the DOM

    • Cleanup
    componentWillUnmount() {
        // Remove listeners
    }
    

React Router

  • BrowserRouter: Track browsing history of the application using the browser's built-in history stack

    • If React Router is used on the web, wrap the application inside of the BrowserRouter component
    import { BrowserRouter } from 'react-router-dom';
    
    const root = ReactDOM.createRoot(
        document.getElementById('app');
    );
    
    root.render(
        <BrowserRouter>
            <App />
        </BrowserRouter>
    );
    
  • For environments that aren't run in the browser, consider:

    • MemoryRouter: Keeps track of the application history on memory rather than the URL. Use this if you're developing a React Native application
    • StaticRouter: Useful in environments where the app's location nevre changes, such as rendering a single route to a static HTML on a server
  • Route allows locations of an application to be mapped to different components

    • For example, a Dashboard component should be rendered when a user navigates to the /dashboard path
    <Route path='/dashboard' element={<Dashboard />} />
    
    • Can wrap multiple Route within Routes for intelligent rendering of children Route elements
    <Routes>
        <Route path='/' element={<Home />} />
        <Route path='/dashboard' element={<Dashboard />} />
        <Route path='*' element={<NotFound />} />
    </Routes>
    
  • Link component enables the application to navigate between Routes
    • to can be passed as an optional object for more control over Link
    • Can add query string via the search property or pass data to the new route via state
    <nav>
        <Link to='/'>Home</Link>
        <Link to='/dashboard'>Dashboard</Link>
        <Link
            to={{
                pathname: '/settings',
                search: '?sort-date',
                state: { fromHome: true }
            }}
        > Settings </Link>
    </nav>
    

Production

  • webpack needs to run in production mode
    • In webpack.config.js:
    mode: process.env.NODE_ENV === 'production' ? 'production' : 'development'
    
    • In package.json:
        "scripts": {
            "dev": "webpack serve",
            "build": "NODE_ENV='production' webpack"
    }