logo

hriram

Dynamic and custom feedback components in React (useDisclose)

In React world, the useDisclose/useDisclosure hook is commonly used to handle open, close and toggle scenarios in feedback components such as modals, alert drawers etc.

The hook commonly implements a state which controls whether the feedback component is open or not (isOpen). One such implementation of the hook is as follows

import React from 'react';

export function useDisclose(initState?: boolean) {
  const [isOpen, setIsOpen] = React.useState(initState || false);
  const onOpen = () => {
    setIsOpen(true);
  };
  const onClose = () => {
    setIsOpen(false);
  };
  const onToggle = () => {
    setIsOpen(!isOpen);
  };
  return {
    isOpen,
    onOpen,
    onClose,
    onToggle,
  };
}

Implementation in Nativebase, a UI library for React Native

The above hook can be used to control the open, close and toggle of feedback components in the application. The useDisclose hook makes it really easy to create the feedback components without coding any logic from scratch. This works well when the number of such components to be used is static.

const {isOpen,onOpen,onClose} = useDisclose();
return (
	<Center>
		<Button onPress={onOpen}>Click to Open!</Button>
		<Actionsheet isOpen={isOpen} onClose={onClose}>
			<Text>Hello from the modal</Text>
		</Actionsheet>
	</Center>
);

Example of Actionsheet which is a drawer component using Nativebase in React Native

Problem statement

Create multiple drawers with one drawer per data point fetched from an external resource. The drawer should display details regarding the data under consideration. Here the trigger could be a button containing the title/label of the data point, which opens the drawer when the button is pressed.

Naive solution

Looking at the problem for the first time, the useDisclose hook seems like the perfect solution. You might implement something like this the first time.

...
//Fetch data from external resource
const dataControls=data?.map((item:ItemInterface)=>{
/*Create useDisclose hook for each data point*/
  let disclose:DiscloseProps=useDisclose();
  item['disclose']=disclose;
  return item;
});
...
const drawer=dataControls.map(item:ItemInterface=>{
/*Render drawer along with button as trigger*/
return (
	<>
		<Button onPress={item.disclose.onOpen}>
			{item.title}
		</Button>
		<Actionsheet 
			isOpen={item.disclose.isOpen} 
			onClose={item.disclose.onClose}
		>
			<Text>{item.description}</Text>
		</Actionsheet>
	</>
});
...
type ItemInterface={
	id:string,
	title:string,
	description:string
}

Interface details

The above solution will work fine when the number of data is static. However if the data fetched from the external resource changes dynamically (addition or removal), we face a nasty error “Rendered more/fewer hooks than expected”

Working solution

The solution I propose makes use of a state variable with datatype Object to achieve the same effect as useDisclose hook mentioned above, but works well with dynamically changing data.

const [disclose,setDisclose]=React.useState({});
React.useEffect(async() => {
/*Fetch data*/
  let discloseInit={};
  data?.forEach((item:ItemInterface)=>{
	/*Set initial open state of all data points as false*/
	  let key=item['id'].toString();
	  discloseInit[key]=false;
 });
  setDisclose(discloseInit);
}, []);
...
const onOpen=(id:string)=>{
	setDisclose({...disclose,[id]:true});  
}
const onClose=(id:string)=>{
  setDisclose({...disclose,[id]:false});
}

As you can see, we have used the id parameter of the item as unique identifier for controlling the opening and closing of the drawer.

State variable disclose at an instant could be as follows

Object {
  "8intnEc70mTzS7RuNcqZ": false,
  "VxQEy0ZGMdJAytvGKt1N": false,
}

Making slight modifications to the UI code already discussed above, the final UI code for the drawer will be as follows

const drawer=dataControls.map(item:ItemInterface=>{
return (
	<>
		<Button onPress={()=>onOpen(item.id)}>{item.title}</Button>
		<Actionsheet 
			isOpen={disclose[item.id]} 
			onClose={()=>onClose(item.id)}
		>
			<Text>{item.description}</Text>
		</Actionsheet>
	</>
});

Demo

A demo of one such implementation of the code above is as follows

image

Thus we have successfully built an extensible, dynamic and custom feedback component using React.