8 Miraculous Ways to Bolster Your React Apps

Sometimes when we build our apps in React we can easily miss opportunities to improve our app, and that's probably because when our app just works and feels fast we tolerate it for being perfect. As developers we might assume that if the outcome of our projects look normal to us, then it would look normal to the users. When our minds think that way, this maycause us to overlook areas in our code that can optimized for a better result.

This article will go over 8 Miraculous Ways to Bolster Your React Apps.

1. Love Your Identities

The first way to bolster your react apps is to love your identities.

It's important to remember that you can wrap variables and functions with React.useMemo as you can grant them the ability to memoize themselves so that react knows they remain identical for future renders.

Otherwise if you don't memoize them, their references will disappear from future renders. This might hurt their feelings, so you can show them that you love them and would like to keep them by memoizing them. If you love them, they'll love you back by making sure they take care of you and your app by helping to avoid wasteful operations for the situations they're in.

For example, let's pretend that we are making a custom hook that takes in a list of urls as arguments so that it can accumulate them into an array of promises to be resolved with Promise.all. The results will be inserted to the state and passed to the App component as soon as it's finished. Our promises list will map over the urls array which contains 4 different urls to fetch:

const authReducer = (state, action) => {
	  switch (action.type) {
	    case 'set-authenticated':
	      return { ...state, authenticated: action.authenticated }
	    default:
	      return state
	  }
	}
export default authReducer

Our task was to fetch data from these 4 links, so ideally only 4 requests should be sent out. But if we take a look at the network tab inside Chrome, the truth is that it sent out 8 requests. This is because the urls argument does not retain the same identity as previously because when App re-renders, it’s instantiating a new array every time–so React treats it as a changed value.

Computer programs sometimes think they can outsmart us and get away with this lousy behavior. In order to fix this we can use React.useMemo so that the array of promises doesn’t recompute itself on every render as long as the array containing the urls does not change.

Let’s refactor our code to apply this concept:

import React from ‘react’

function useWhyDidYouUpdate(name, props) {
  // Get a mutable ref object where we can store props ...
  // ... for comparison next time this hook runs.
  const previousProps = useRef()


  useEffect(() => {
    if (previousProps.current) {
      // Get all keys from previous and current props
      const allKeys = Object.keys({ ...previousProps.current, ...props })
      // Use this object to keep track of changed props
      const changesObj = {}
      // Iterate through keys
      allKeys.forEach((key) => {
        // If previous is different from current
        if (previousProps.current[key] !== props[key]) {
          // Add to changesObj
          changesObj[key] = {
            from: previousProps.current[key],
            to: props[key],
          }
        }
      })


      // If changesObj not empty then output to console
      if (Object.keys(changesObj).length) {
        console.log('[why-did-you-update]', name, changesObj)
      }
    }


    // Finally update previousProps with current props for next hook call
    previousProps.current = props
  })
}


export default useWhyDidYouUpdate

If we run this now, it will still be sending 8 requests. That’s because although we memoized the urls array, we also need to memoize the promisesvariables inside our hook because that is also instantiating itself whenever the hook runs:

import { toast } from ‘react-toastify’
import {
info as toastInfo,
success as toastSuccess,
toastIds,
} from ‘util/toast’
import App from ‘./App’

const Root = () => {
  const onOnline = () => {
    if (toast.isActive(toastIds.internetOffline)) {
      toast.dismiss(toastIds.internetOffline)
    }
    
    if (toast.isActive(toastIds.retryInternet)) {
      toast.dismiss(toastIds.retryInternet)
    }
    if (!toast.isActive(toastIds.internetOnline)) {
      toastSuccess('You are now reconnected to the internet.', {
        position: 'bottom-center',
        toastId: toastIds.internetOnline,
      })
    }
  }


  const onOffline = () => {
    if (!toast.isActive(toastIds.internetOffline)) {
      toastInfo('You are disconnected from the internet right now.', {
        position: 'bottom-center',
        autoClose: false,
        toastId: toastIds.internetOffline,
      })
    }
  }
  
  useInternet({ onOnline, onOffline })
  
  return <App />
}

Our code should now only send 4 requests when we run it. Hurray!

2. Merge Props to Children

Sometimes we can get in a situation where we want to sneak in a prop to be merged with children before proceeding to render. React lets you view the props of any react element as well as others, such as exposing its key.

We can just wrap the children element with a new component and inject the new props from there or we can just merge the new props by using this method.

For example, lets say we have an App component that is using a useModalhook which provides some handy utilities to manage modals by providing controls like openclose, and opened. We want to pass these props to a VisibilityControl component because it’s going to provide additional functionality before passing the modal data down to the children:

import React from ‘react’

const UserContext = React.createContext({
  user: {
    firstName: 'Kelly',
    email: 'frogLover123@gmail.com',
  },
  activated: true,
})


const VisibilityControl = ({ children, opened, close }) => {
  const ctx = React.useContext(UserContext)
  return React.cloneElement(children, {
    opened: ctx.activated ? opened : false,
    onClick: close,
  })
}


export const useModal = ({ urls } = {}) => {
  const [opened, setOpened] = React.useState(false)
  const open = () => setOpened(true)
  const close = () => setOpened(false)


  return {
    opened,
    open,
    close,
  }
}


const App = ({ children }) => {
  const modal = useModal()


  return (
    <div>
      <button type="button" onClick={modal.opened ? modal.close : modal.open}>
        {modal.opened ? 'Close' : 'Open'} the Modal
      </button>
      <VisibilityControl {...modal}>{children}</VisibilityControl>
    </div>
  )
}


const Window = ({ opened }) => {
  if (!opened) return null
  return (
    <div style={{ border: '1px solid teal', padding: 12 }}>
      <h2>I am a window</h2>
    </div>
  )
}


export default () => (
  <App>
    <Window />
  </App>
)

VisibilityControl makes sure that activated is true before allowing opened to be used normally by its children. If this was used in a secret route the VisibilityControl provides the functionality of preventing unactivated users from seeing secret content.

3. Combine Reducers for a Gargantuan Reducer

There might come a time when you need to combine two or more reducers in the app to make a larger one. This approach is similar to how combineReducers worked in react-redux.

Let’s pretend we were planning to make a giant microservice app where we originally planned to designate each part in the app to be in charge of their own context/state but then we just thought of a million dollar idea of an app that requires the states to be united into a single large state instead so we can manage them all in the same environment.

We have an authReducer.jsownersReducer.js, and frogsReducer.js that we’d like to combine:

authReducer.js

const authReducer = (state, action) => {
switch (action.type) {
case ‘set-authenticated’:
return { …state, authenticated: action.authenticated }
default:
return state
}
}

export default authReducer

ownersReducer.js

const ownersReducer = (state, action) => {
switch (action.type) {
case ‘add-owner’:
return {
…state,
profiles: […state.profiles, action.owner],
}
case ‘add-owner-id’:
return { …state, ids: […state.ids, action.id] }
default:
return state
}
}

export default ownersReducer

frogsReducer.js

const frogsReducer = (state, action) => {
switch (action.type) {
case ‘add-frog’:
return {
…state,
profiles: […state.profiles, action.frog],
}
case ‘add-frog-id’:
return { …state, ids: […state.ids, action.id] }
default:
return state
}
}

export default frogsReducer

We’ll import them into our main file and define the state structure there:

App.js

import React from ‘react’
import authReducer from ‘./authReducer’
import ownersReducer from ‘./ownersReducer’
import frogsReducer from ‘./frogsReducer’

const initialState = {
  auth: {
    authenticated: false,
  },
  owners: {
    profiles: [],
    ids: [],
  },
  frogs: {
    profiles: [],
    ids: [],
  },
}


function rootReducer(state, action) {
  return {
    auth: authReducer(state.auth, action),
    owners: ownersReducer(state.owners, action),
    frogs: frogsReducer(state.frogs, action),
  }
}


const useApp = () => {
  const [state, dispatch] = React.useReducer(rootReducer, initialState)


  const addFrog = (frog) => {
    dispatch({ type: 'add-frog', frog })
    dispatch({ type: 'add-frog-id', id: frog.id })
  }


  const addOwner = (owner) => {
    dispatch({ type: 'add-owner', owner })
    dispatch({ type: 'add-owner-id', id: owner.id })
  }


  React.useEffect(() => {
    console.log(state)
  }, [state])


  return {
    ...state,
    addFrog,
    addOwner,
  }
}


const App = () => {
  const { addFrog, addOwner } = useApp()


  const onAddFrog = () => {
    addFrog({
      name: 'giant_frog123',
      id: 'jakn39eaz01',
    })
  }


  const onAddOwner = () => {
    addOwner({
      name: 'bob_the_frog_lover',
      id: 'oaopskd2103z',
    })
  }


  return (
    <>
      <div>
        <button type="button" onClick={onAddFrog}>
          add frog
        </button>
        <button type="button" onClick={onAddOwner}>
          add owner
        </button>
      </div>
    </>
  )
}

export default () => <App />

You would then just work with hooks as you normally would with calling dispatch, passing in the matching type and arguments to the designated reducer.

The most important part to look at is the rootReducer:

function rootReducer(state, action) {
return {
auth: authReducer(state.auth, action),
owners: ownersReducer(state.owners, action),
frogs: frogsReducer(state.frogs, action),
}
}

4. Sentry for Error Reports

Projects benefit immensely from Sentry when integrated with React. Having detailed reports of errors all sent to a central location to be analyzed at once is a very important tool to have!

Once you npm install @sentry/browser and have it set up for your react app, you can log into sentry.io after you create your account and analyze your error reports in your project’s dashboard.

These reports are really detailed, so you’ll benefit from feeling like an FBI agent by getting fed tons of information to help you solve those errors like knowing the user’s device, browser, the URL the error occurred, the user’s IP address, the stack trace of the error, was the error handled or not, the function name, the source code, a useful list of breadcrumbs exposing a trace of network actions that led to the error, headers, and more.

Here’s a screenshot of what this may look like:

You can also have several team members comment on different things so it can also be a collaborative environment.

5. Use axios over window.fetch

Unless you don’t care about Internet Explorer users, you should not use window.fetch for your react apps because none of the IE browsers support window.fetch unless you provide a polyfill. Axios is great to support IE but also good for the additional functionality it brings to the table like canceling requests mid-flight. This window.fetch actually applies to any web app and not specific to React. The reason it’s in this list is because it’s not uncommon that window.fetch is used in React apps today. And since react apps goes through transpiling/compiling stages depending on the tools configured, it can be quite tempting to accidentally assume that it transpiles window.fetch.

6. Use Callback Refs Over Object Refs When Watching DOM Nodes

Even though React.useRef is the new kid on the block for attaching and controlling references to a DOM node, it’s not always the best option.

Sometimes you might need more control to a DOM node so that you can provide additional functionality.

For example, the react docs show a situation where you’ll need to use a callback ref to ensure that even when there are changes to the current ref value, a component outside can still be notified of updates. This is the advantage of callback refs over useRef.

Material-ui makes use of this powerful concept to attach additional functionality throughout their component modules. The great part about this is that cleanup naturally emerges from this behavior. Wonderful!

7. useWhyDidYouUpdate

This is a custom hook to expose changes that make our components re-render. Sometimes when a memoizer like the higher order component React.memo isn’t enough you can use this handy hook to find which props you need to consider memoizing instead

import React from ‘react’

function useWhyDidYouUpdate(name, props) {
  // Get a mutable ref object where we can store props ...
  // ... for comparison next time this hook runs.
  const previousProps = useRef()


  useEffect(() => {
    if (previousProps.current) {
      // Get all keys from previous and current props
      const allKeys = Object.keys({ ...previousProps.current, ...props })
      // Use this object to keep track of changed props
      const changesObj = {}
      // Iterate through keys
      allKeys.forEach((key) => {
        // If previous is different from current
        if (previousProps.current[key] !== props[key]) {
          // Add to changesObj
          changesObj[key] = {
            from: previousProps.current[key],
            to: props[key],
          }
        }
      })


      // If changesObj not empty then output to console
      if (Object.keys(changesObj).length) {
        console.log('[why-did-you-update]', name, changesObj)
      }
    }


    // Finally update previousProps with current props for next hook call
    previousProps.current = props
  })
}


export default useWhyDidYouUpdate

You would then use it like this:

const MyComponent = (props) => {
useWhyDidYouUpdate(‘MyComponent’, props)

  return <Dashboard {...props} />
}

8. Make Your Functions Find You

This is going to be quoted from my previous article from awhile ago because it’s a little long and it fits greatly in this post. Here is the content:

Let me give a real life example as I’d like to put a little more emphasize on this one.

One of the greatest benefits of higher order functions is that when used correctly, it will save a lot of time for you and for those around you.

At my job, we used react-toastify to display notifications. We used it every where. In addition, they also make great escape hatches for quick last minute UX decisions: “How should we handle this error? Just display a toast notification!” Done.

However, we started noticing that when the app became larger and the level of complexity was creeping up on us, our toast notifications were becoming too frequent. This is fine–however, we didn’t have a way of preventing duplicates. This meant that some toast notifications were showing up multiple times on the screen even when they were exactly the same as the toast above it.

So we ended up leveraging the api that the library provides to help remove active toast notifications by id using toast.dismiss().

In order to explain the parts ahead, it’s probably a good idea to show the file we were importing toasts from before proceeding:

import React from ‘react’
import { GoCheck, GoAlert } from ‘react-icons/go’
import { FaInfoCircle } from ‘react-icons/fa’
import { MdPriorityHigh } from ‘react-icons/md’
import { toast } from ‘react-toastify’

/*
  Calling these toasts most likely happens in the UI 100% of the time.
  So it is safe to render components/elements as toasts.
*/


// Keeping all the toast ids used throughout the app here so we can easily manage/update over time
// This used to show only one toast at a time so the user doesn't get spammed with toast popups
export const toastIds = {
  // APP
  internetOnline: 'internet-online',
  internetOffline: 'internet-offline',
  retryInternet: 'internet-retry',
}


// Note: this toast && is a conditional escape hatch for unit testing to avoid an error.
const getDefaultOptions = (options) => ({
  position: toast && toast.POSITION.BOTTOM_RIGHT,
  ...options,
})


const Toast = ({ children, success, error, info, warning }) => {
  let componentChildren
  // Sometimes we are having an "object is not valid as a react child" error and children magically becomes an API error response, so we must use this fallback string
  if (!React.isValidElement(children) && typeof children !== 'string') {
    componentChildren = 'An error occurred'
  } else {
    componentChildren = children
  }
  let Icon = GoAlert
  if (success) Icon = GoCheck
  if (error) Icon = GoAlert
  if (info) Icon = FaInfoCircle
  if (warning) Icon = MdPriorityHigh
  return (
    <div style={{ paddingLeft: 10, display: 'flex', alignItems: 'center' }}>
      <div style={{ width: 30, height: 30 }}>
        <Icon style={{ color: '#fff', width: 30, height: 30 }} />
      </div>
      <div style={{ padding: 8, display: 'flex', alignItems: 'center' }}>
          
        <span style={{ color: '#fff' }}>{componentChildren}</span>
      </div>
    </div>
  )
}


export const success = (msg, opts) => {
  return toast.success(<Toast success>{msg}</Toast>, {
    className: 'toast-success',
    ...getDefaultOptions(),
    ...opts,
  })
}


export const error = (msg, opts) => {
  return toast.error(<Toast error>{msg}</Toast>, {
    className: 'toast-error',
    ...getDefaultOptions(),
    ...opts,
  })
}


export const info = (msg, opts) => {
  return toast.info(<Toast info>{msg}</Toast>, {
    className: 'toast-info',
    ...getDefaultOptions(),
    ...opts,
  })
}


export const warn = (msg, opts) => {
  return toast.warn(<Toast warning>{msg}</Toast>, {
    className: 'toast-warn',
    ...getDefaultOptions(),
    ...opts,
  })
}


export const neutral = (msg, opts) => {
  return toast(<Toast warning>{msg}</Toast>, {
    className: 'toast-default',
    ...getDefaultOptions(),
    ...opts,
  })
}

Now bear with me, I know this might not look appealing. But I promise it will get better in two minutes.

This is what we had in a separate component to check if a previous toast was already on the screen. And if there was, it will attempt to remove that toast and re-display the new toast.

import { toast } from ‘react-toastify’
import {
info as toastInfo,
success as toastSuccess,
toastIds,
} from ‘util/toast’
import App from ‘./App’

const Root = () => {
  const onOnline = () => {
    if (toast.isActive(toastIds.internetOffline)) {
      toast.dismiss(toastIds.internetOffline)
    }
    
    if (toast.isActive(toastIds.retryInternet)) {
      toast.dismiss(toastIds.retryInternet)
    }
    if (!toast.isActive(toastIds.internetOnline)) {
      toastSuccess('You are now reconnected to the internet.', {
        position: 'bottom-center',
        toastId: toastIds.internetOnline,
      })
    }
  }


  const onOffline = () => {
    if (!toast.isActive(toastIds.internetOffline)) {
      toastInfo('You are disconnected from the internet right now.', {
        position: 'bottom-center',
        autoClose: false,
        toastId: toastIds.internetOffline,
      })
    }
  }
  
  useInternet({ onOnline, onOffline })
  
  return <App />
}

This was working fine–however, we had other toasts throughout the app that needed to be modified the same way. We had to go through every file that displays a toast nofication to remove duplicates.

When we think of going through every file in 2019, we immediately knew that it wasn’t the solution. So we looked at the util/toast.js file and refactored that to solve our problem instead. Here’s what it looked like afterwards:

src/util/toast.js
import React, { isValidElement } from ‘react’
import isString from ‘lodash/isString’
import isFunction from ‘lodash/isFunction’
import { GoCheck, GoAlert } from ‘react-icons/go’
import { FaInfoCircle } from ‘react-icons/fa’
import { MdPriorityHigh } from ‘react-icons/md’
import { toast } from ‘react-toastify’

/*
  Calling these toasts most likely happens in the UI 100% of the time.
  So it is safe to render components/elements as toasts.
*/


// Keeping all the toast ids used throughout the app here so we can easily manage/update over time
// This used to show only one toast at a time so the user doesn't get spammed with toast popups
export const toastIds = {
  // APP
  internetOnline: 'internet-online',
  internetOffline: 'internet-offline',
  retryInternet: 'internet-retry',
}


// Note: this toast && is a conditional escape hatch for unit testing to avoid an error.
const getDefaultOptions = (options) => ({
  position: toast && toast.POSITION.BOTTOM_RIGHT,
  ...options,
})


const Toast = ({ children, success, error, info, warning }) => {
  let componentChildren
  // Sometimes we are having an "object is not valid as a react child" error and children magically becomes an API error response, so we must use this fallback string
  if (!isValidElement(children) && !isString(children)) {
    componentChildren = 'An error occurred'
  } else {
    componentChildren = children
  }
  let Icon = GoAlert
  if (success) Icon = GoCheck
  if (error) Icon = GoAlert
  if (info) Icon = FaInfoCircle
  if (warning) Icon = MdPriorityHigh
  return (
    <div style={{ paddingLeft: 10, display: 'flex', alignItems: 'center' }}>
      <div style={{ width: 30, height: 30 }}>
        <Icon style={{ color: '#fff', width: 30, height: 30 }} />
      </div>
      <div style={{ padding: 8, display: 'flex', alignItems: 'center' }}>
          
        <span style={{ color: '#fff' }}>{componentChildren}</span>
      </div>
    </div>
  )
}


const toaster = (function() {
  // Attempt to remove a duplicate toast if it is on the screen
  const ensurePreviousToastIsRemoved = (toastId) => {
    if (toastId) {
      if (toast.isActive(toastId)) {
        toast.dismiss(toastId)
      }
    }
  }
  // Try to get the toast id if provided from options
  const attemptGetToastId = (msg, opts) => {
    let toastId
    if (opts && isString(opts.toastId)) {
      toastId = opts.toastId
    } else if (isString(msg)) {
      // We'll just make the string the id by default if its a string
      toastId = msg
    }
    return toastId
  }
  const handleToast = (type) => (msg, opts) => {
    const toastFn = toast[type]
    if (isFunction(toastFn)) {
      const toastProps = {}
      let className = ''
      const additionalOptions = {}
      const toastId = attemptGetToastId(msg, opts)
      if (toastId) additionalOptions.toastId = toastId
      // Makes sure that the previous toast is removed by using the id, if its still on the screen
      ensurePreviousToastIsRemoved(toastId)
      // Apply the type of toast and its props
      switch (type) {
        case 'success':
          toastProps.success = true
          className = 'toast-success'
          break
        case 'error':
          toastProps.error = true
          className = 'toast-error'
          break
        case 'info':
          toastProps.info = true
          className = 'toast-info'
          break
        case 'warn':
          toastProps.warning = true
          className - 'toast-warn'
          break
        case 'neutral':
          toastProps.warning = true
          className - 'toast-default'
          break
        default:
          className = 'toast-default'
          break
      }
      toastFn(<Toast {...toastProps}>{msg}</Toast>, {
        className,
        ...getDefaultOptions(),
        ...opts,
        ...additionalOptions,
      })
    }
  }
  return {
    success: handleToast('success'),
    error: handleToast('error'),
    info: handleToast('info'),
    warn: handleToast('warn'),
    neutral: handleToast('neutral'),
  }
})()


export const success = toaster.success
export const error = toaster.error
export const info = toaster.info
export const warn = toaster.warn
export const neutral = toaster.neutral

Instead of having to go through every file, the simplest solution was to create a higher order function. Doing this allowed us to “reverse” the roles so that instead of us searching through files, the toasts were instead directed to our higher order function.

This way the codes in the files were not modified or touched. They still function as normal, and we gained the ability to remove duplicate toasts without going anywhere to write unnecessary code in the end. This saved time.

Conclusion

And that concludes the end of this article! I hope you found it useful and look out for more in the future!

Thanks for reading. If you liked this post, share it with all of your programming buddies!

Further reading

☞ React - The Complete Guide (incl Hooks, React Router, Redux)

☞ Modern React with Redux [2019 Update]

☞ Best 50 React Interview Questions for Frontend Developers in 2019

☞ JavaScript Basics Before You Learn React

☞ Microfrontends — Connecting JavaScript frameworks together (React, Angular, Vue etc)

☞ Reactjs vs. Angularjs — Which Is Best For Web Development

☞ React + TypeScript : Why and How

☞ How To Write Better Code in React

☞ React Router: Add the Power of Navigation

☞ Getting started with React Router

☞ Using React Router for optimizing React apps


#reactjs #react-native #javascript #node-js #web-development

8 Miraculous Ways to Bolster Your React Apps
13.95 GEEK