How to build a generic list component in React - reactjs

Im building a user profile which renders 4 lists:
reviews
saved posts.
created posts
active posts.
2-4 are lists rendering the same component and 1 is a a list that renders a different component - ReviewCard.
As far as I know, there's some way to make a generic component to render a list of components by type or something of the sort.
Each list also has it's own states and additional sub-components like for example star container in reviews which doesnt exists in the others.
Right now my code is extremely messy:
useEffect(() => {
if (type === 'reviews') {
fetchReviews()
}
dispatch(fetchNewRoom())
}, [])
useEffect(() => {
setInitialMessages(messages?.slice(0, 3))
}, [messages])
useEffect(() => {
setInitialRooms(rooms?.slice(0, 3))
}, [rooms])
where rooms, reviews and messages are 3 lists that i want to render on profile. im sure there's a better practice for that.
Would be great to discuss ideas.
Thanks for helping out

In your use-case, there are four list which could be rendered inside a container, where container will have list and common config. In case if there is config which is not constant then pass props and handle it accordingly. Below is the code snippet.
//Below is the list container
const ListContainer = (props) => {
const { listData } = props
const getListItem = (type) => {
switch(type) {
case 'review':
return <review/>
case 'spost'
return <spost/>
case 'cpost'
return <cpost/>
case 'apost'
return <apost/>
default:
return null;
}
}
return (
<div>
{listData.map((item) => {
return getListItem(item.type)
})}
</div>
)
}
//This is the profile component where we you can render 4 list simultaneously.
import ListContainer from './listContainer'
const Profile = () => {
return (
<ListContainer type="review" />
<ListContainer type="spost" />
<ListContainer type="cpost" />
<ListContainer type="apost" />
)
}

Related

Expose a Custom Hook to Children (children only) in React

I'm not sure the title is correct, so let me try to explain what I'm trying to achieve.
Let's say I have a flow in my application that has 3 steps in it, so I create a component (let's call it Stepper) with 3 child components where each child is a component that renders the corresponding step.
I want to expose a custom hook to the child components of Stepper, let's call it useStepper.
This is how Stepper would look like (JSX-wise):
export const Stepper = (props) => {
...some logic
return (
<SomeWrapper>
{props.children}
</SomeWrapper>
);
};
so I can make components like this:
export SomeFlow = () => {
return (
<Stepper>
<StepOne />
<StepTwo />
<StepThree />
</Stepper>
);
};
Now this is how I want things to work inside Stepper's children, let's take StepThree as an example:
export const StepThree = () => {
const exposedStepperData = useStepper();
... some logic
return (
...
);
};
Now, it's important that the Stepper will be reusable; That means - each Stepper instance should have its own data/state/context that is exposed through the useStepper hook.
Different Stepper instances should have different exposed data.
Is it possible to achieve this? I tried to use Context API but I was not successful. It's also weird that I couldn't find anything about it on the internet, maybe I searched wrong queries as I don't know what patten it is (if it exists).
Note:
I achieved a similar behavior through injected props from parent to its children, but it's not as clean as I want it to be, especially with Typescript.
I recently came across something like this, it was solved by pouring all the components/steps in an array and let the hook manage which component/step to show. If you want it to be more reusable you could pass in the children to the array.
I hope this helps you in the right direction
useStepper.ts
import { ReactElement, useState } from "react";
export const useStepper = (steps: ReactElement[]) => {
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const next = () => {
setCurrentStepIndex((i: number) => {
if (i >= steps.length - 1) return i;
return i + 1;
});
};
const back = () => {
setCurrentStepIndex((i: number) => {
if (i <= 0) return i;
return i - 1;
});
};
const goTo = (index: number) => {
setCurrentStepIndex(index);
};
return {
currentStepIndex,
step: steps[currentStepIndex],
steps,
isFirstStep: currentStepIndex === 0,
isLastStep: currentStepIndex === steps.length - 1,
goTo,
next,
back,
};
};
Stepper.tsx
// const { currentStepIndex, step, isFirstStep, isLastStep, back, next } =
// useStepper([<StepOne />, <StepTwo />, <StepThree />]);
const { currentStepIndex, step, isFirstStep, isLastStep, back, next } =
useStepper([...children]);
return (
<div>
{!isFirstStep && <button onClick={back}>Back</button>}
{step}
<button onClick={next}>{isLastStep ? "Finish" : "Next"}</button>
</div>
);

Passing data to a Child Component React

I have read:
react passing data from parent to child component
and
https://www.freecodecamp.org/news/pass-data-between-components-in-react/
While they outline the desired method. It does not seem to be working for the following example. I must be doing something obvious, but I cannot seem to figure out what I am doing wrong.
Structure
The structure is a wizard style form (4 steps) below which is contained in newrequest.js. Below each step is a list table component with various items associated with each step.
Think of it like a shopping cart, except you can have multiple carts (step 1), you can leave them in the store filled with all sorts of things (step 2-3) for quite some time before buying anything (step 4).
A user many want to create several carts all at the time, and then add/create/edit other items to the carts over the course of several months before finalizing. The carts, and the items in those carts need to stay grouped together, and they need to be found easily enough by the user to keep working on them at some later date.
User Action:
The user goes to step 1, either makes a new cart, or grabs an existing cart to edit and proceeds to step 2. For the edit option, I have been able to pass the id of an existing cart (list-request-saved.js) to (newrequest.js), edit, save etc. I can also tie the items to the cart with a cartid prop when this is happening the first time.
The Problem:
The issue is I cannot seem to figure out how to pass the cartid akarequestID, to step 2 when editing an existing cart akaSavedRequest so the database can fetch the items which belong in that cart. Without the cartid, the database just spits out all items in any cart by anyone. To fix this, I would need to get the cartid to "newrequest.js" from the child component "list-requests-saved.js " in step 1 (This works ok) and then pass the cartid to child component "list-components.js" in step 2 (This does not work ok).
newrequest.js (parent) // Removed the non-impacting sections
//queries
import { listTestRequests } from '../../graphql/queries'
import { listComponents } from '../../graphql/queries'
import { getSavedRequest } from '../../graphql/queries'
import { getComponent } from '../../graphql/queries'
//components
import SavedRequests from "../../pages/list-requests-saved"
import Components from "../../pages/list-components"
const initialTestRequestState = getInitialState(listTestRequests)
const initialComponentState = getInitialState(listComponents)
function NewRequest(props) {
const [testRequest, setTestRequest] = React.useState(initialTestRequestState)
const [component, setComponent] = React.useState(initialComponentState)
const [editRequest, setEditRequest] = React.useState('')
const [editComponent, setEditComponent] = React.useState('')
const requestTableToParent = (requestID) => {
setEditRequest(requestID)
}
const componentTableToParent = (componentID) => {
setEditComponent(componentID)
}
useEffect(() => {
fetchRequest(editRequest)
async function fetchRequest(id) {
if (!id) return
const requestData = await API.graphql({ query: getSavedRequest, variables: { id } })
setTestRequest(requestData.data.getSavedRequest)
}
}, [editRequest])
useEffect(() => {
fetchComponent(editComponent)
async function fetchComponent(id) {
if (!id) return
const componentData = await API.graphql({ query: getComponent, variables: { id } })
setComponent(componentData.data.getComponent)
}
}, [editComponent])
return (
<form
autoComplete="off"
noValidate
{...props}
>
{
activeStep == 0
? <SavedRequests requestTableToParent={requestTableToParent}/>
: null
}
{
activeStep == 1
? <Components componentTableToParent={componentTableToParent} requestID={editRequest}/>
: null
}
</form >
)
}
export default NewRequest
list-requests-saved.js (child-step-1) // Removed the non-impacting sections
function SavedRequests({requestTableToParent}) {
const renderEditButton = (params) => {
return (
<strong>
<Button
variant="text"
color="primary"
size="small"
style={{ marginLeft: 16 }}
onClick={() => requestTableToParent(params.row.id)}
>
Edit
</Button>
</strong>
)
}
export default (SavedRequests)
list-components.js (child-step-2) // Removed the non-impacting sections
import { listComponents } from '../graphql/queries'
import { deleteComponents as deleteComponentsMutation } from '../graphql/mutations'
function Components({componentTableToParent}, {requestID}) {
const [component, setComponent] = useState([])
const classes = useStyles();
console.log("requestID", requestID) //this is undefined
}
export default (Components)
Additional Commentary: editRequest in "newrequest.js" has the correct value which was set by the edit button in "list-requests-saved.js". When I'm trying to catch the value in the child component "list-components.js", requestID is reporting as undefined. What am I doing wrong?

When are components defined in functions evaluated? (React Hooks)

Suppose I have a component that renders a list item:
const ListItem = ({ itemName }) => {
return (
<div>
{itemName}
</div>
)
}
And because this list item is used in many places in my app, I define a custom hook to render the list and control the behavior of each list instance:
const useListItems = () => {
const [ showList, setShowList ] = useState(true)
const { listItemArray, isLoaded } = useListContext() // Context makes api call
const toggleShowList = setShowList(!showList)
function renderListItems() {
return isLoaded && !!listItemArray ? listItemArray.map((itemName, index) => (
<ListItem key={index} isVisible={showList} itemName={itemName}/>
))
:
null
}
// Some other components and logic...
return {
// ...Other components and logic,
renderListItems,
toggleShowList,
}
}
My first question is, when will the array of ListItems actually be evaluated ? Will the jsx resulting from renderListItems() be calculated every time renderListItems() is called? Or would it happen every time useListItems() is called?
Second question: if I call useListItems() in another component but don't call renderListItems(), does that impact whether the components are evaluated?
I have been struggling to find an answer to this, so thanks in advance.

How to show dinamically a react component

I'm working in a React project and I need to render some Components based on a layout.
--asumme you have an array that tells you the components you need to render:
Layout1 = ['Events', 'Photo', 'News']
--And a function that, depends on the module, render the especific component with some data:
layout1.forEach(function(layout) {
someFuncions(layout, somedata);
});
someFunction = (layout, data) => {
layout.forEach( function(Comp) {
if(Comp == 'Events') {
return (<Events module-data={data} />)
} else if(Comp == 'Photo') {
return (<Photo module-data={data} />)
} else if(Comp == 'News') {
return (<News module-data={data} />)
}
});
}
-- Since it is possible to have many components I want to find a way to avoid the "if" function to determine what component should I render.
How can I achieve this? Thanks a lot guys
You can use a switch similar syntex but maybe a little cleaner.
switch(comp) {
case 'Events':
return (<Events module-data={data} />)
case 'Photo':
return (<Photo module-data={data} />)
case 'News':
return (<News module-data={data} />)
}
You can pass the component in the array directly.
import Events from 'path/to/Events'
import Photo from 'path/to/Photo'
import News from 'path/to/News'
Layout1 = [Events, Photo, News]
And then wherever you call the function.
From your original question, it seems like the layout1 in your question is an array of Layout1 like arrays.
layout1.forEach(function(layout) {
someFuncions(layout, somedata);
});
someFunction = (layout, data) => {
// returning from forEach won't result in anything. you probably want to map
layout.forEach( function(Component) {
return (<Component module-data={data} />)
})
}
I would approaching it by creating a map for your components, then doing a lookup when rendering.
const components = {
Events, // Shorthand for 'Events': Events
Photo,
News
};
const layout = ['Events', 'Photo'];
const someFunction = (layout, data) =>
layout.map((name) => {
const Component = components[name];
return <Component module-data={data} />;
});
What about using a javascript object that maps the layout to the desired component? This way, it's clear that you intend to render a different component depending on the layout.
See the following example:
const someFunction = (layout, data) => {
const componentMap = {
'Events': <Events module-data={data} />,
'Photo': <Photo module-data={data} />,
'News': <News module-data={data} />
}
return componentMap[layout];
}

UseEffect causes infinite loop with swipeable routes

I am using the https://www.npmjs.com/package/react-swipeable-routes library to set up some swipeable views in my React app.
I have a custom context that contains a dynamic list of views that need to be rendered as children of the swipeable router, and I have added two buttons for a 'next' and 'previous' view for desktop users.
Now I am stuck on how to get the next and previous item from the array of modules.
I thought to fix it with a custom context and custom hook, but when using that I am getting stuck in an infinite loop.
My custom hook:
import { useContext } from 'react';
import { RootContext } from '../context/root-context';
const useShow = () => {
const [state, setState] = useContext(RootContext);
const setModules = (modules) => {
setState((currentState) => ({
...currentState,
modules,
}));
};
const setActiveModule = (currentModule) => {
// here is the magic. we get the currentModule, so we know which module is visible on the screen
// with this info, we can determine what the previous and next modules are
const index = state.modules.findIndex((module) => module.id === currentModule.id);
// if we are on first item, then there is no previous
let previous = index - 1;
if (previous < 0) {
previous = 0;
}
// if we are on last item, then there is no next
let next = index + 1;
if (next > state.modules.length - 1) {
next = state.modules.length - 1;
}
// update the state. this will trigger every component listening to the previous and next values
setState((currentState) => ({
...currentState,
previous: state.modules[previous].id,
next: state.modules[next].id,
}));
};
return {
modules: state.modules,
setActiveModule,
setModules,
previous: state.previous,
next: state.next,
};
};
export default useShow;
My custom context:
import React, { useState } from 'react';
export const RootContext = React.createContext([{}, () => {}]);
export default (props) => {
const [state, setState] = useState({});
return (
<RootContext.Provider value={[state, setState]}>
{props.children}
</RootContext.Provider>
);
};
and here the part where it goes wrong, in my Content.js
import React, { useEffect } from 'react';
import { Route } from 'react-router-dom';
import SwipeableRoutes from 'react-swipeable-routes';
import useShow from '../../hooks/useShow';
import NavButton from '../NavButton';
// for this demo we just have one single module component
// when we have real data, there will be a VoteModule and CommentModule at least
// there are 2 important object given to the props; module and match
// module comes from us, match comes from swipeable views library
const ModuleComponent = ({ module, match }) => {
// we need this function from the custom hook
const { setActiveModule } = useShow();
// if this view is active (match.type === 'full') then we tell the show hook that
useEffect(() => {
if (match.type === 'full') {
setActiveModule(module);
}
},[match]);
return (
<div style={{ height: 300, backgroundColor: module.title }}>{module.title}</div>
);
};
const Content = () => {
const { modules, previousModule, nextModule } = useShow();
// this is a safety measure, to make sure we don't start rendering stuff when there are no modules yet
if (!modules) {
return <div>Loading...</div>;
}
// this determines which component needs to be rendered for each module
// when we have real data we will switch on module.type or something similar
const getComponentForModule = (module) => {
// this is needed to get both the module and match objects inside the component
// the module object is provided by us and the match object comes from swipeable routes
const ModuleComponentWithProps = (props) => (
<ModuleComponent module={module} {...props} />
);
return ModuleComponentWithProps;
};
// this renders all the modules
// because we return early if there are no modules, we can be sure that here the modules array is always existing
const renderModules = () => (
modules.map((module) => (
<Route
path={`/${module.id}`}
key={module.id}
component={getComponentForModule(module)}
defaultParams={module}
/>
))
);
return (
<div className="content">
<div>
<SwipeableRoutes>
{renderModules()}
</SwipeableRoutes>
<NavButton type="previous" to={previousModule} />
<NavButton type="next" to={nextModule} />
</div>
</div>
);
};
export default Content;
For sake of completion, also my NavButton.js :
import React from 'react';
import { NavLink } from 'react-router-dom';
const NavButton = ({ type, to }) => {
const iconClassName = ['fa'];
if (type === 'next') {
iconClassName.push('fa-arrow-right');
} else {
iconClassName.push('fa-arrow-left');
}
return (
<div className="">
<NavLink className="nav-link-button" to={`/${to}`}>
<i className={iconClassName.join(' ')} />
</NavLink>
</div>
);
};
export default NavButton;
In Content.js there is this part:
// if this view is active (match.type === 'full') then we tell the show hook that
useEffect(() => {
if (match.type === 'full') {
setActiveModule(module);
}
},[match]);
which is causing the infinite loop. If I comment out the setActiveModule call, then the infinite loop is gone, but of course then I also won't have the desired outcome.
I am sure I am doing something wrong in either the usage of useEffect and/or the custom hook I have created, but I just can't figure out what it is.
Any help is much appreciated
I think it's the problem with the way you are using the component in the Route.
Try using:
<Route
path={`/${module.id}`}
key={module.id}
component={() => getComponentForModule(module)}
defaultParams={module}
/>
EDIT:
I have a feeling that it's because of your HOC.
Can you try
component={ModuleComponent}
defaultParams={module}
And get the module from the match object.
const ModuleComponent = ({ match }) => {
const {type, module} = match;
const { setActiveModule } = useShow();
useEffect(() => {
if (type === 'full') {
setActiveModule(module);
}
},[module, setActiveModule]);
match is an object and evaluated in the useEffect will always cause the code to be executed. Track match.type instead. Also you need to track the module there. If that's an object, you'll need to wrap it in a deep compare hook: https://github.com/kentcdodds/use-deep-compare-effect
useEffect(() => {
if (match.type === 'full') {
setActiveModule(module);
}
},[match.type, module]);

Resources