Reusable logic with React Render Props

In this post, I want to introduce you RenderProps: what is it and how to use it. RenderProps is a pattern, as well as HOC (higher order component) it's designed to pack logic into a component for further reuse where required.

Simply said, it is a component that takes props, one of which must be a function. Calling this function, we can pass the data by arguments that will be available in the component from which the function was passed.

You can transfer data from the involving component to the RenderProps component via props. In a HOC case, we can only pass static data as arguments.

Short description: ({ children }) => { logic... return children(args) }

Imagine we have two pages where each needs to fetch user data. The logic for each page is repeating so we will create a ProvideAuth component which provides the user profile and loading state.

An example of RenderProps usage with "children as prop":

  const ProvideAuth = ({ children }) => {
    // state
    const [userProfile, setUserProfile] = React.useState({ isAuthorized: false, data: {} })
    const [isUserLoading, setUserLoadingState] = React.useState(false)
  
    const handleSetUserLoading = value => {
      setUserLoadingState(value)
    }
  
    React.useEffect(() => {
      handleGetUser()
    }, [])
  
    const handleGetUser = async () => {
      try {
        handleSetUserLoading(true)
  
        const response = await getUser()
  
        setUserProfile({ isAuthorized: true, data: response.data })
      } catch (error) {
        console.log('Error while User preload:', error)
      } finally {
        handleSetUserLoading(false)
      }
    }
  
    if (!userProfile.isAuthorized && !isUserLoading) {
      return <div>U're not authorized</div>
    }
  
    return (
      <>
        {isUserLoading ? (
          <div>Loading...</div>
        ) : (
          <>
            {/* call children function and provide data */}
            {children({ userProfile, isUserLoading })}
          </>
        )}
      </>
    )
  }
  
  const PageFirst = () => (
    <ProvideAuth>
      {/* provide a function as children and return ReactNode */}
      {({ userProfile }) => (
        <>
          <div>Your First Name: {userProfile.data.firstName}</div>
          <div>Your Last Name: {userProfile.data.lastName}</div>
          <div>Is Authorized: {userProfile.isAuthorized ? 'Yes' : 'No'}</div>
        </>
      )}
    </ProvideAuth>
  )
  
  const PageSecond = () => (
    <ProvideAuth>
      {/* provide a function as children and return ReactNode */}
      {({ userProfile }) => (
        <div>
          Your Full Name: {userProfile.data.firstName} {userProfile.data.lastName}
        </div>
      )}
    </ProvideAuth>
  )
  

If RenderProps (ProvideAuth) wraps elements within render, a function that returns children is specified instead of children as ReactNode(s). The data passed from the ProvideAuth are the arguments for this function. Thus, unlike a standard container where children can be ReactNode(s), we pass a function, once called, returns a ReactNode. That 's all the magic RenderProps.

Instead of children, we can pass the function as props with a common name and return the ReactNode as well.

Example of using RenderProps with "custom prop":

  const ProvideAuth = ({ renderAuth }) => {
    // state
    const [userProfile, setUserProfile] = React.useState({ isAuthorized: false, data: {} })
    const [isUserLoading, setUserLoadingState] = React.useState(false)
  
    const handleSetUserLoading = value => {
      setUserLoadingState(value)
    }
  
    React.useEffect(() => {
      handleGetUser()
    }, [])
  
    const handleGetUser = async () => {
      try {
        handleSetUserLoading(true)
  
        const response = await getUser()
  
        setUserProfile({ isAuthorized: true, data: response.data })
      } catch (error) {
        console.log('Error while User preload:', error)
      } finally {
        handleSetUserLoading(false)
      }
    }
  
    if (!userProfile.isAuthorized && !isUserLoading) {
      return <div>U're not authorized</div>
    }
  
    return (
      <>
        {isUserLoading ? (
          <div>Loading...</div>
        ) : (
          <>
            {/* call renderAuth prop function and provide data */}
            {renderAuth({ userProfile, isUserLoading })}
          </>
        )}
      </>
    )
  }
  
  const PageFirst = () => (
    <ProvideAuth
      // provide prop renderAuth function and return ReactNode
      renderAuth={({ userProfile }) => (
        <>
          <div>Your First Name: {userProfile.data.firstName}</div>
          <div>Your Last Name: {userProfile.data.lastName}</div>
          <div>Is Authorized: {userProfile.isAuthorized ? 'Yes' : 'No'}</div>
        </>
      )}
    />
  )
  
  const PageSecond = () => (
    <ProvideAuth
      // provide prop renderAuth function and return ReactNode
      renderAuth={({ userProfile }) => (
        <div>
          Your Full Name: {userProfile.data.firstName} {userProfile.data.lastName}
        </div>
      )}
    />
  )
  

As a matter of experience, I can say RenderProps is ideal for creating UI modules that can be reused in different projects. They can be easily adapted to the needs of each project where applicable. That is very important for development acceleration.