Skip to main content

Sharing Logic Between Components in Different React Renderers

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.

Sharing Logic Between Components in Different React Renderers

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.

Initial folder structure for the app
Initial folder structure for the app.
Initial UI for the new project
The initial UI for the new project

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.


Related Posts

The BrainG-UX Philosophy The BrainG-UX Philosophy

The BrainG-UX Philosophy

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.