Loading...

Sharing Logic Between Components in Different React Renderers

7 min read

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()
	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()
	 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.

Who Is BrainGu?

BrainGu develops custom DevSecOps software that enables mission success and boasts exceptional user and developer experience by working directly with end-users to solve their real-life problems and continuously improve capabilities. By automating pinch points, BrainGu innovates new ways to solve mission problems.

Our vision is to solve complex national security challenges for the United States and its allies by incubating and scaling technology solutions that emphasize fielded, meaningful military capability in the hands of operators and mission owners.

BrainGu is setting the standard for rapid deployment and scalability of mission applications. As part of our Mission App as a Service solution offering, BrainGu offers subscription and packaged app timeline products that are aligned to BrainGu’s overall mission to provide the best, cutting-edge technology to the warfighter at the tactical edge.

About Us