React Rendering for Rapidly Changing UIs
This article presents situations where imperative programming can be used to improve performance when updating multiple UI components.
The bottom line is that there are more targets for React and it may still be necessary to reuse logic across multiple renderers for different platforms. Fortunately, building a custom hook provides an easier path to share component logic than the original render props technique did.
Background
Several years ago, I wrote the article Sharing Code Between React Web and React Native Applications. That article demonstrated how to use render props to share component logic in a way that a React JS application and a React Native application could use the same business logic while rendering separate React JS and React Native views.
Since that article was published, there have been enough changes in the React community and the tools available to justify a re-write. Some of these changes include the shift from class-based to functional components, the introduction and growing importance of hooks, and the growing number of renderers that allow React developers to output to platforms other than the web and mobile.
The bottom line is that there are more targets for React and it may still be necessary to reuse logic across multiple renderers for different platforms. Fortunately, building a custom hook provides an easier path to share component logic than the original render props technique did.
This article discusses how to use a custom hook to share logic between two components; one component targeting react-dom to render HTML and another component that uses the react-pdf renderer primitives for use in creating a larger PDF component.
This article is focused on sharing component logic and does not include a deep dive into React nor the react-pdf renderer so you should refer to those packages’ documentation if you need familiarization with them.
Extracting Functionality to A Custom React Hook
Let’s start by using vitejs to quickly scaffold a simple React project that includes a component suitable for a small demonstration on how to extract logic into a custom hook.
The component includes a counter that uses the useState
hook to track and update
the number of times the button is pressed.
function App() {
const [count, setCount] = useState(0)
All of the logic in the initial component.
If we want to use the same logic in another component we can start by extracting any logic that component requires into a hook in a separate file. The purpose of this hook is to provide any state and the functions a component requires to interact with that state.
import React, { useState } from "react";
const useApp = () => {
const [count, setCount] = useState(0);
return { count, setCount };
};
export default useApp;
Our new hook containing all of the component logic.
Now, instead of using useState
directly, the App
component pulls
logic and state from the “useApp” hook as shown below.
function App() {
const { count, setCount } = useApp();
App now gets its state from the custom useApp hook.
At this point, any component we make can make use of the useApp
hook. This is
especially useful for a React Native View that includes the same button pressing logic. Since the user can
still interact with the button and expects to see the count changed.
Using a Custom Hook to in Components Targeting Different Renderers
Now that we have a demonstration of how to extract and build a custom hook, let's look at how this technique is useful to simplify creating a PDF component to use with react-pdf renderer.
The chatPostThreadInfo
component below has multiple pieces of state and several
useEffect to build the JSX that renders HTML.
const ChatPostThreadInfo = ({ post }: { post: Post }) => {
// component state
// ...
const dispatch = useDispatch()
// state items needed by the JSX and useEffects
const [parentPost, setParentPost] = useState()
const [parentPoster, setParentPoster] = useState<bot |="" userprofile="">()
const [parentPostBlurb, setParentPostBlurb] = useState('')
const [isMounted, setIsMounted] = useState(false)
useEffect(() => { … }, [])
useEffect(() => { … }, [post.parent_id, posts])
useEffect(() => { … }, [parentPost?.message])
useEffect(() => { … }, [bots, myUserProfile, parentPost, userProfiles])
const onClickBlurb = async () => {... }
return (
Commented on {parentPoster.username}'s message:
{parentPostBlurb}
)
}
Original component that renders HTML and includes state logic.
In order to use the same state logic to build a PDF component, all the stateful logic can be extracted to a custom hook that updates and provides state that the presentational components need.
const useChatPostThreadInfo = ({ post }: Props) => {
// other internal state state
// ...
const dispatch = useDispatch()
// state items needed by the JSX and useEffects
const [parentPost, setParentPost] = useState()
const [parentPoster, setParentPoster] = useState<bot |="" userprofile="">()
const [parentPostBlurb, setParentPostBlurb] = useState('')
const [isMounted, setIsMounted] = useState(false)
useEffect(() => { … }, [])
useEffect(() => { … }, [post.parent_id, posts])
useEffect(() => { … }, [parentPost?.message])
useEffect(() => { … }, [bots, myUserProfile, parentPost, userProfiles])
// return the state that the presentational components need
return { parentPost, parentPoster, parentPostBlurb }
}
export default useChatPostThreadInfo
Custom useChatThreadInfo
hook to provide state to presentational
components.
After the extraction, the original component that renders HTML can be updated to retrieve the necessary state from the custom hook.
const ChatPostThreadInfo = ({ post }: { post: Post }) => {
// other state items…
// obtain state from the new custom hook
const { parentPost, parentPoster, parentPostBlurb } = useChatPostThreadInfo({ post })
// note that this function remains in this HTML component since the PDF component will not need it
const onClickBlurb = async () => { ... }
return (
Commented on {parentPoster.username}'s message:
{parentPostBlurb}
)
}
export default ChatPostThreadInfo
Updated HTML component that now uses the useChatThreadInfo
hook.
Next, the custom hook can be used to provide the same data to the component that will render the PDF version of the component.
const ChatPostThreadInfoPdf = ({ post }: { post: Post }) => {
// obtain state from the new custom hook
const { parentPost, parentPoster, parentPostBlurb } = useChatPostThreadInfo({ post })
return (
{Commented on ${parentPoster.username}'s message: ${parentPostBlurb}
}
)
}
export default ChatPostThreadInfoPdf
PDF component that uses the useChatThreadInfo
hook.
Conclusion
In the end, we can see that extracting the component logic that needs to be shared into a custom hook provides a simpler method for sharing logic and state between components than the original render props technique. This results in logic that can be shared across multiple renderers.
This article presents situations where imperative programming can be used to improve performance when updating multiple UI components.
An authentic user experience goes far beyond giving customers what they say they want. So we go deeper and research the underlying wants and needs of the end-user. To provide a high-quality user experience, we have to be seamless in our disciplines, which we discuss in detail.
Girls Who Code is changing the game. They're reaching girls worldwide and are on track to close the gender gap in new entry-level tech jobs by 2027.