Looking for a clever way to render multiple children component dynamically - reactjs

I have a Modal component composed of a <Header> and a <Content>.
I want both children <Header> and <Content> to be rendered dynamically based on a given value ('modal type').
My first intuition was to create an object storing modal components like:
export const useModalDetails = (onClose = () => {}) => {
const [ modalDetails, setModalDetails ] = useMergeState({
Header: () => <div/>,
Content: () => <div/>,
onClose,
});
return [modalDetails, setModalDetails];
};
Then a function to set modal details base on the value type:
const onChangeModalType = (type) => {
if (type === 'type1') {
setModalDetails({
Header: () => <div>Modal Header Type 1</dov>,
Content: () => <div>Modal Content Type 1</div>,
open: true
})
} else if (type === 'type2') {
setModalDetails({
Header: () => <div>Modal Header Type 2</dov>,
Content: () => <div>Modal Content Type 2</div>,
open: true
})
}
}
Then I can use the react-hook in the <Modal> component:
import SemanticModal from 'semantic-ui-react'
const Modal = () => {
const [ modalDetails, setModalDetails ] = useMergeState({
Header: () => <></>,
Content: () => <></>,
open: false,
});
return [modalDetails, setModalDetails];
onChangeModalType('type2')
return (
<>
<SemanticModal
Header={modalDetails.Header}
Content={modalDetails.Content}
open={modalDetails.open}
/>
<button onClick={() => onChangeModalType('type1')}>load type 1</button>
<button onClick={() => onChangeModalType('type2')}>load type 2</button>
</>
)
}
Bear in mind I have 12 different types of content, and header for a <Modal>, that means 12 if, else statements for each.
Do you have any idea how I could develop that in a smarter, clever, more readable, maintainable... WAY?

If you're decided on having an individual modal component that switches behaviour based on state/props you could approach it like this:
const modals = {
type1: {
Header: <div>Modal1 Header</div>,
Content: <div>Modal1 Content</div>,
open: true,
},
type2: {
Header: <div>Modal2 Header</div>,
Content: <div>Modal2 Content</div>,
open: true,
},
};
const Modal = ({type}) => {
const [modalType, setModalType] = useState(type);
const { Header, Content, open } = modals[modalType];
return (
<>
<SemanticModal
Header={Header}
Content={Content}
open={open}
/>
</>
);
};
An alternative to consider is creating a reusable BaseModal component which you extend into a seperate component for each use case:
const BaseModal = ({ Header, Content, open }) => {
return (
<>
<SemanticModal Header={Header} Content={Content} open={open} />
</>
);
};
const Modal1 = () => {
return (
<BaseModal
Header={<div>Modal1 Header</div>}
Content={<div>Modal1 Content</div>}
open={true}
/>
);
};
The nice thing about this pattern is that you still get to share the common elements of all modals whilst selectively injecting the parts that need to differ

Related

How to properly change the boolean inside of object in array?

So, I'm trying toggle the Icon based on the isBadData per email data in the object of array. But I can't seem to find out how could save it back to the state so it can update the Icon image in LeadProfileComponent.
This is what it looks like:
checkIcon = isBadData: false
crossIcon = isBadData: true
Heres my code:
// ModalComponent.js
const [leadProfile, setLeadProfile] = useState([
{
id: 'd114877b-074b-4aa2-a3f0-3b9446885336',
firstName: 'wqe',
lastName: 'wqe',
name: 'wqe wqe',
email: [
{
type: 'personal',
address: 'qwe#hotmail.com',
valid_since: '2010-05-09',
isBadData: true,
},
{
type: 'personal',
address: 'wqe#hotmail.com',
valid_since: '2017-03-09',
isBadData: true,
},
{
type: 'personal',
address: 'wqe#aol.com',
valid_since: '2009-01-12',
isBadData: true,
},
],
},
]);
<LeadProfileComponent leadProfile={leadProfile} setLeadProfile={setLeadProfile} />
// LeadProfileComponent.js
const LeadProfileComponent = (props) => {
const handleChildEmail = (email, index) => {
props.setLeadProfile((prev: any) => {
const value = { ...prev[0].email[index] };
console.log('inside value');
console.log(value);
value.isBadData = !value.isBadData;
console.log(value);
// return prev;
return [value];
});
console.log('props.leadProfile');
console.log(props.leadProfile);
};
return (
<>
{
props.leadProfile.map((lead, index) => (
return(
<>
{lead.email.map(() => {
return (
<button
id="btnCheck"
onClick={() => {
handleChildEmail(email, index);
}}
>
<img
src={
email.isBadData !== true
? checkIcon
: closeIcon
}
/>
</button>
)
})}
</>
)
}
</>
);
}
Heres what it looks like when you console log inside of handChildEmail function:
As you can see, I was able to change the inside boolean of email[0], but I cant save it back to the leadProfile state since I have a missing part in the destructuring part
Break your components in smaller parts, and manage each email individually
LeadProfileEmailComponent.js
const LeadProfileEmailComponent = ({ initialEmailData, ...props }) => {
const [emailData, setEmailData] = useState(initialEmailData);
return (
<button
id="btnCheck"
onClick={() => {
setEmailData({
...emailData,
isBadData: !emailData.isBadData
});
}}
>
<img
src={
emailData.isBadData !== true
? checkIcon
: closeIcon
}
/>
</button>
)
}
Change this in LeadProfileComponent:
{lead.email.map((email) => {
return (
<LeadProfileEmailComponent initialEmailData={email} />
)
})}
The downside is, the state of the parent component will not be updated. However this is standard design pattern practise, you should not rely on the parent component data for this.

Ternary operator shows for milliseconds the truthy part even when it's false

(sorry for my English)
I'm new in Reactjs and I'm trying to do an application where the user can create Flashcards.
The flashcards must have a category so, when the user enter in the menu to list and create flashcards, if there isn't any category created, I want to show a phrase for the user to access before the category menu to create a category.
To do that, my component that lists de flashcards receive an Prop with the list of categories and if the list is empty, it show the banner, if not, it show the rest of the component:
type Props = {
categorias: Categoria[];
};
const TarjetasList = ({ categorias }: Props) => { ...
return (
<>
{!categorias.length ? (
<h1>Debe crear una categoria</h1>
) : (
<>
<TarjetaFilter onSubmitFilter={handleSubmitFilter} />
<div>
{page?.content.map((tarjeta) => (
<div key={tarjeta.id}>
<TarjetaCard tarjeta={tarjeta} onDelete={getTarjetas} />
</div>
))}
</div>
<Pagination
pageCount={page ? page?.totalPages : 0}
range={2}
onChange={handlePageChange}
/>
</>
)}
</>
);
};
The problem is that when the user accesses the menu that lists the cards, it shows the banner with the phrase "You must create a category" for a few milliseconds, even though the "!categoria.length" is false, and then it stops display it and it displays the rest of the component as it should, but even though it's only for a few milliseconds, I want to avoid that. Is it possible?
What am I doing wrong?
here is the code of the component of the menu that opens de list of cards:
const Tarjetas = () => {
const [categorias, setCategorias] = useState<Categoria[]>([]);
const getCategorias = useCallback(() => {
const config: AxiosRequestConfig = {
method: "GET",
url: "/categorias",
withCredentials: true,
};
requestBackend(config).then((response) => {
setCategorias(response.data.content);
});
}, []);
useEffect(() => {
getCategorias();
}, [getCategorias]);
return (
<Routes>
<Route index element={<TarjetasList categorias={categorias} />} />
<Route path=":tarjetaId" element={<TarjetasForm />} />
</Routes>
);
};
Here is the complete code of the TarjetasList component:
type ControlComponentsData = {
activePage: number;
filterData: TarjetaFilterData;
};
type Props = {
categorias: Categoria[];
};
const TarjetasList = ({ categorias }: Props) => {
const [page, setPage] = useState<SpringPage<Tarjeta>>();
const [controlComponentsData, setControlComponentsData] =
useState<ControlComponentsData>({
activePage: 0,
filterData: { texto: "", categoria: null },
});
const getTarjetas = useCallback(() => {
const config: AxiosRequestConfig = {
method: "GET",
url: "/tarjetas",
withCredentials: true,
params: {
page: controlComponentsData.activePage,
size: 3,
categoriaId: controlComponentsData.filterData.categoria?.id,
texto: controlComponentsData.filterData.texto,
},
};
requestBackend(config).then((response) => {
setPage(response.data);
});
}, [controlComponentsData]);
useEffect(() => {
getTarjetas();
}, [getTarjetas]);
const handlePageChange = (pageNumber: number) => {
setControlComponentsData({
activePage: pageNumber,
filterData: controlComponentsData.filterData,
});
};
const handleSubmitFilter = (data: TarjetaFilterData) => {
setControlComponentsData({
activePage: 0,
filterData: data,
});
};
return (
<>
{!categorias.length ? (
<h1>Debe crear una categoria</h1>
) : (
<>
<TarjetaFilter onSubmitFilter={handleSubmitFilter} />
<div>
{page?.content.map((tarjeta) => (
<div key={tarjeta.id}>
<TarjetaCard tarjeta={tarjeta} onDelete={getTarjetas} />
</div>
))}
</div>
<Pagination
pageCount={page ? page?.totalPages : 0}
range={2}
onChange={handlePageChange}
/>
</>
)}
</>
);
};
Your categories categorias are initially empty. Until you load them, you will be displaying the text.
The simplest possible solution is not to initialise categorias to empty list but to null:
const [categorias, setCategorias] = useState<Categoria[] | null>(null);
Then you can add an additional rendering state until categorias are loaded:
if (!categorias) {
return <h1>Cargando…</h1>
}
return (
<>
{!categorias.length ? (
<h1>Debe crear una categoria</h1>
) : (
...
However, ideally you would handle loading state using complex structures similar to:
{ isLoading: true, error: null, categorias: [] }
Then you would be able to correctly distinguish when to display loading indication and when to display loading error.

Infinite call renderCell in React

I'm using function component to create a MUI dataGrid, and trying to add a button in a column, and I have a onRowClick function to open a side pane when user clicking row. The problem is, once I click row, react will report error:
Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
Here is the code:
const openViewPane = (params: GridRowParams, e): void => {
setRightSlidePlaneContent(
<ViewAccountPane
close={closeForm}
params={params}
/>,
);
setRightSlidePlaneOpen(true);
};
const formatDates = (columns): GridColDef[] => {
return columns;
};
const addTooltipsToData = (columns: GridColDef[]): GridColDef[] => {
console.log('render tool bar called');
return columns.map((column) => {
const { description, field, headerName } = column;
console.log('inside map');
if (field === ID) {
console.log('直接return');
return column;
}
return {
...column,
renderCell: (): JSX.Element => {
console.log('render run');
return (
<Tooltip arrow title={description || ''} >
<span className={classes.headerCell}>{headerName}</span>
</Tooltip>
);
},
};
});
};
const formatColumns = (columns: GridColDef[]): GridColDef[] => {
const dateFormatted = formatDates(columns);
return addTooltipsToData(dateFormatted);
};
console.log('generic table rendered');
return (
<MuiThemeProvider theme={theme}>
<DataGrid
columns={formatColumns(columns)}
rows={rows}
autoHeight
className={classes.table}
components={{
Toolbar: CustomToolbar,
}}
density={GridDensityTypes.Compact}
filterMode={tableMode}
hideFooterSelectedRowCount
loading={loading}
onFilterModelChange={handleFilterChange}
onSortModelChange={handleSortChange}
sortModel={sortModel}
sortingMode={tableMode}
onRowClick={openViewPane}
/>
</MuiThemeProvider>
);
However, if I change the renderCell to renderHeader, it will work fine.
setRightSlidePlaneContent
setRightSlidePlaneOpen
Above are two state passed by parent component in props. it will open a slide pane.
After I comment setRightSliePlaneOpen, it will work well. But no slide pane show.
Please help me slove it. Or do you know how can I add a button in column not using renderCell?
const PageFrame: FC<IProps> = (props: IProps) => {
const classes = useStyles();
const dispatch = useAppDispatch();
const { Component, userInfo } = props;
const [navBarOpen, setNavBarOpen] = useState(false);
const [rightSlidePlaneOpen, setRightSlidePlaneOpen] = useState(false);
const [rightSlidePlaneContent, setRightSlidePlaneContent] = useState(
<Fragment></Fragment>,
);
const [rightSlidePlaneWidthLarge, setRightSlidePlaneWidthLarge] = useState(
false,
);
useEffect(() => {
dispatch({
type: `${GET_USER_LOGIN_INFO}_${REQUEST}`,
payload: {
empId: userInfo.empId,
auth: { domain: 'GENERAL_USER', actionType: 'GENERAL_USER', action: 'VIEW', empId: userInfo.empId},
},
meta: { remote: true },
});
}, []);
return (
<div className={classes.root}>
<HeaderBar
navBarOpen={navBarOpen}
toggleNavBarOpen={setNavBarOpen}
/>
<NavigationBar open={navBarOpen} toggleOpen={setNavBarOpen} />
<Component
setRightSlidePlaneContent={setRightSlidePlaneContent}
setRightSlidePlaneOpen={setRightSlidePlaneOpen}
setRightSlidePlaneWidthLarge={setRightSlidePlaneWidthLarge}
/>
<PersistentDrawerRight
content={rightSlidePlaneContent}
open={rightSlidePlaneOpen}
rspLarge={rightSlidePlaneWidthLarge}
/>
</div>
);
};
export default PageFrame;
The component that calls setRightSidePlaneOpen
interface IProps {
setRightSlidePlaneContent: React.Dispatch<React.SetStateAction<JSX.Element>>;
setRightSlidePlaneOpen: React.Dispatch<React.SetStateAction<boolean>>;
setRightSlidePlaneWidthLarge: React.Dispatch<SetStateAction<boolean>>;
}
const TagDashboard = (props: IProps): JSX.Element => {
const { setRightSlidePlaneContent, setRightSlidePlaneOpen, setRightSlidePlaneWidthLarge } = props;
const employeeId = useAppSelector((store) => store.userInfo.info.employeeNumber);
const rows = useAppSelector((state) => state.tag.rows);
const accountId = useAppSelector(store => store.userInfo.accountId);
const updateContent = useAppSelector(state => state.tag.updateContent);
const numOfUpdates = useAppSelector(state => state.tag.numOfUpdates);
const dispatch = useAppDispatch();
const closeAddForm = (): void => {
setRightSlidePlaneContent(<Fragment />);
setRightSlidePlaneOpen(false);
};
const openAddForm = (): void => {
setRightSlidePlaneContent(
<AddForm
category={'tag'}
close={closeAddForm}
title={ADD_FORM_TITLE}
createFunction={createTag}
/>);
setRightSlidePlaneOpen(true);
};
const closeForm = (): void => {
setRightSlidePlaneContent(<Fragment />);
setRightSlidePlaneOpen(false);
setRightSlidePlaneWidthLarge(false);
};
const openViewPane = (params: GridRowParams, e): void => {
setRightSlidePlaneContent(
<ViewAccountPane
close={closeForm}
params={params}
/>,
);
setRightSlidePlaneOpen(true);
setRightSlidePlaneWidthLarge(true);
};
// to the RSP.
return (
<GenericDashboard
addFunction={openAddForm}
description={DESCRIPTION}
title={TITLE}
columns={columns}
handleRowClick={openViewPane}
rows={rows}
numOfUpdates={numOfUpdates}
updateContent={updateContent}
/>
);
};
This is the component of the right slide pane
const { content, open, rspLarge } = props;
const classes = useStyles();
const drawerClass = rspLarge ? classes.drawerLarge : classes.drawer;
const drawerPaperClass = rspLarge ? classes.drawerPaperLarge : classes.drawerPaper;
return (
<div className={classes.root}>
<CssBaseline />
<Drawer
className={drawerClass}
variant='temporary'
anchor='right'
open={open}
classes={{
paper: drawerPaperClass,
}}
>
<Fragment>{content}</Fragment>
</Drawer>
</div>
);

Best way to use useMemo/React.memo for an array of objects to prevent rerender after edit one of it?

I'm struggling with s performance issue with my React application.
For example, I have a list of cards which you can add a like like facebook.
Everything, all list is rerendering once one of the child is updated so here I'm trying to make use of useMemo or React.memo.
I thought I could use React.memo for card component but didn't work out.
Not sure if I'm missing some important part..
Parent.js
const Parent = () => {
const postLike= usePostLike()
const listData = useRecoilValue(getCardList)
// listData looks like this ->
//[{ id:1, name: "Rose", avararImg: "url", image: "url", bodyText: "text", liked: false, likedNum: 1, ...etc },
// { id:2, name: "Helena", avararImg: "url", image: "url", bodyText: "text", liked: false, likedNum: 1, ...etc },
// { id: 3, name: "Gutsy", avararImg: "url", image: "url", bodyText: "text", liked: false, likedNum: 1, ...etc }]
const memoizedListData = useMemo(() => {
return listData.map(data => {
return data
})
}, [listData])
return (
<Wrapper>
{memoizedListData.map(data => {
return (
<Child
key={data.id}
data={data}
postLike={postLike}
/>
)
})}
</Wrapper>
)
}
export default Parent
usePostLike.js
export const usePressLike = () => {
const toggleIsSending = useSetRecoilState(isSendingLike)
const setList = useSetRecoilState(getCardList)
const asyncCurrentData = useRecoilCallback(
({ snapshot }) =>
async () => {
const data = await snapshot.getPromise(getCardList)
return data
}
)
const pressLike = useCallback(
async (id) => {
toggleIsSending(true)
const currentList = await asyncCurrentData()
...some api calls but ignore now
const response = await fetch(url, {
...blabla
})
if (currentList.length !== 0) {
const newList = currentList.map(list => {
if (id === list.id) {
return {
...list,
liked: true,
likedNum: list.likedNum + 1,
}
}
return list
})
setList(newList)
}
toggleIsSending(false)
}
},
[setList, sendYell]
)
return pressLike
}
Child.js
const Child = ({
postLike,
data
}) => {
const { id, name, avatarImg, image, bodyText, likedNum, liked } = data;
const onClickPostLike = useCallback(() => {
postLike(id)
})
return (
<Card>
// This is Material UI component
<CardHeader
avatar={<StyledAvatar src={avatarImg} />}
title={name}
subheader={<SomeImage />}
/>
<Image drc={image} />
<div>{bodyText}</div>
<LikeButton
onClickPostLike={onClickPostLike}
liked={liked}
likedNum={likedNum}
/>
</Card>
)
}
export default Child
LikeButton.js
const LikeButton = ({ onClickPostLike, like, likedNum }) => {
const isSending = useRecoilValue(isSendingLike)
return (
<Button
onClick={() => {
if (isSending) return;
onClickPostLike()
}}
>
{liked ? <ColoredLikeIcon /> : <UnColoredLikeIcon />}
<span> {likedNum} </span>
</Button>
)
}
export default LikeButton
The main question here is, what is the best way to use Memos when one of the lists is updated. Memorizing the whole list or each child list in the Parent component, or use React.memo in a child component...(But imagine other things could change too if a user edits them. e.g.text, image...)
Always I see the Parent component is highlighted with React dev tool.
use React.memo in a child component
You can do this and provide a custom comparator function:
const Child = React.memo(
({
postLike,
data
}) => {...},
(prevProps, nextProps) => prevProps.data.liked === nextProps.data.liked
);
Your current use of useMemo doesn't do anything. You can use useMemo as a performance optimization when your component has other state updates and you need to compute an expensive value. Say you have a collapsible panel that displays a list:
const [expand, setExpand] = useState(true);
const serverData = useData();
const transformedData = useMemo(() =>
transformData(serverData),
[serverData]);
return (...);
useMemo makes it so you don't re-transform the serverData every time the user expands/collapses the panel.
Note, this is sort of a contrived example if you are doing the fetching yourself in an effect, but it does apply for some common libraries like React Apollo.

How to add a function in const Target = props => {

How do I add a function to connect to one of my components onChange? Creating a function like this returns an error code of 'cardActionResponse' is not defined.
What the benefit of using a const class like this?
const Target = props => {
const { markAsDone } = useContext(ItemContext);
const [{ isOver }, drop] = useDrop({
accept: 'Item',
drop: (item, monitor) => console.log(item),
collect: monitor => ({
isOver: !!monitor.isOver()
})
})
//Cannot do this. How else can I make a function to connect to CreateVideoCard?
cardActionResponse = (event) => {
console.log(event);
}
return (
<div className="target top80 right30" ref={drop} style={{ backgroundColor: isOver ? 'black' : '' }} >
<TitleDescription class="z1"/>
<div class="right10 left10">
<CreateVideoCard onChange={this.cardActionResponse} />
<CreateDescriptionCard></CreateDescriptionCard>
<CreateAudioCard></CreateAudioCard>
<CreateTermsCard></CreateTermsCard>
</div>
</div>
);
};
export default Target;
Functional components don't have it's own context (this), so you should simply use const variable.
Please use
const cardActionResponse = (event) => {
console.log(event);
}
and then
<CreateVideoCard onChange={cardActionResponse} />

Resources