Sharing data/state between 2 sibling components without rerendering - reactjs

I am building a web app with next.js and am using the PowerBI client to render an iframe with statistics. I can interact with the iframe via the library to update filters and pages. This means the iframe should NOT rerender everytime a prop changes.
The parent component looks like this.
const Dashboard: NextPage = ({ data }) => {
const [activePage, setActivePage] = useState(null);
const DynamicDashboard = dynamic(
() =>
import("../components/dynamicDashboard").then(
(dashboard) => dashboard.default
),
{ ssr: false }
);
return (
<>
<Header setActivePage={setActivePage} />
<DynamicDashboard data={data} activePage={activePage} />
</>
);
};
The header (child 1) looks like this:
export default function ({ setActivePage }) {
const menuItems = [
{
displayName: "page1",
name: "ReportSection5ac5ce426571489b0038",
},
{
displayName: "page2",
name: "ReportSectionf3695be54d17769ba015",
},
{
displayName: "page3",
name: "ReportSection60bb35142132a98c6b86",
},
];
const menuItemClickHandler = (name) => {
setActivePage(name);
};
return (
<>
<header className={styles.header}>
<div className={styles.inner}>
<div className={styles.logo}>
<img src="/assets/images/logo.svg" alt="logo" />
</div>
<ul className={styles.menu}>
{menuItems.map((item) => (
<li key={item.name}>
<a href="#" onClick={() => menuItemClickHandler(item.name)}>
{item.displayName}
</a>
</li>
))}
</ul>
</div>
</header>
</>
);
}
DynamicDashboard (child 2) receives the prop "activePage" like this.
To keep everything simple and focussed on the question I'll keep the contents of this component empty.
export default function DynamicDashboard({ data, activePage }) {}
I understand that the rerender happens because I'm using useState.
However when I want to use useRef I get the message that I'm not allowed to use a ref on a function component.
So basically I want to update the DynamicDashboard component based on events in the Header component without rerendering the whole component.

Related

React passing data from generated pages to other components

I am creating pages dynamically per product id and I am able to access the data via pageContext within each page. The product pages are generating correctly with the data expected, however I am struggling with the logic of passing the data to other components eg the shopping cart. How do I pass the data across?
gatsby-node.js
const products = require('./src/helpers/product.json');
products.forEach(product => {
createPage({
path: `/product/${product.productCode}/`,
component : require.resolve('./src/templates/product.js'),
context : {
title: product.title,
description: product.description,
image: product.image,
price: product.price,
productCode: product.productCode,
}
})
})
The Products page has various components such as QuickView of the item, MiniCartView, add to Cart- all of which I would like to share the product data across the components.
Product.js (reduced code snippet)
const Product = ({ pageContext }) => {
const ctxAddItemNotification = useContext(AddItemNotificationContext);
const showNotification = ctxAddItemNotification.showNotification;
return (
<Layout>
<div className={styles.root}>
<Container size={'large'} spacing={'min'}>
<Breadcrumbs
crumbs={[
{ link: '/', label: 'Home' },
{ label: `${pageContext.name}` },
]}
/>
<div className={styles.content}>
<div className={styles.gallery}>
<Gallery images={pageContext.gallery} />
</div>
<div className={styles.details}>
<h1>{pageContext.name}</h1>
<div className={styles.actionContainer}>
<div className={styles.addToButtonContainer}>
<Button
onClick={() => { showNotification() }}
fullWidth
level={'primary'}
setProductID={setProductID}>
Add to Bag
</Button>
</div>
</div>
</div>
}
I am using a template and trying to add functionality but unable to work out how to pass data through to the other components. The miniCart component has hardcoded mock data but I want to pass in the data per product which was retrieved from the json file.
MiniCart.js
const MiniCart = ({props}) => {
const sampleCartItem = {
image: '',
alt: '',
name: 'name',
price: 220,
color: 'Anthracite Melange',
size: 'xs',
};
return (
<div className={styles.root}>
<div className={styles.titleContainer}>
<h4>My Bag</h4>
</div>
</div>
....
The <MiniCart> reference is within another component (header.js). Levels above the Product.js, therefore I was unable to use to pass the selected product's data across.
header.js
<Drawer visible={showMiniCart} close={() => setShowMiniCart(false)}>
<MiniCart />
</Drawer>
If anyone is able to help I would greatly appreciate :)

How can I create Single Page

How can I pass map items (title, category and images) in my id.jsx file.
Basically, I just want to create a single page for my projects. But I can only access post ID. I don't know how to pass other data items.
'Projects folder'
[id].js
import { useRouter } from "next/router";
const Details = () => {
const router = useRouter();
return <div>Post #{router.query.id}
// Single Project Title = {project.title} (like this)
</div>;
};
export default Details;
index.js
import { MyProjects } from "./MyProjects";
const Projects = () => {
const [projects, setProjects] = useState(MyProjects);
{projects.map((project) => (
<Link
href={"/projects/" + project.id}
key={project.id}
passHref={true}
>
<div className="project__item">
<div className="project__image">
<Image src={project.image} alt="project" />
</div>
<div className="project_info">
<h5>{project.category}</h5>
<h3>{project.title}</h3>
</div>
</div>
</Link>
))}
If I understand your question correctly, you want to send some "state" along with the route transition. This can be accomplished using an href object with the "state" on the query property, and the as prop to hide the query string.
Example:
{projects.map((project) => (
<Link
key={project.id}
href={{
pathname: "/projects/" + project.id,
query: {
id: project.id,
category: project.category,
title: project.title
}
}}
passHref={true}
as={"/projects/" + project.id}
>
<div className="project__item">
<div className="project__image">
<Image src={project.image} alt="project" />
</div>
<div className="project_info">
<h5>{project.category}</h5>
<h3>{project.title}</h3>
</div>
</div>
</Link>
))}
...
const Details = () => {
const router = useRouter();
return (
<div>
<div>Post #{router.query.id}</div>
<div>Title {router.query.title}</div>
<div>Category {router.query.category}</div>
</div>
);
};

Dynamically rendering child components in react

I'm using firestore database to store my data in the collection "listings". So for each document in "listings", I need to render a <BookListing/> element in Home.js with the data from each document. From my research, there are a few other questions similar to this one out there, but they're outdated and use different react syntax. Here's my code:
function BookListing({id, ISBN, title, image, price}) {
return (
<div className="bookListing">
<div className='bookListing_info'>
<p className="bookListing_infoTitle">{title}</p>
<p className="bookListing_infoISBN"><span className="bookListing_infoISBNtag">ISBN: </span>{ISBN}</p>
<p className="bookListing_infoPrice">
<small>$</small>
{price}
</p>
</div>
<img className="bookListing_img" src={image} alt=""></img>
<button className="bookListing_addToCart">Add to Cart</button>
</div>
)
}
export default BookListing
function Home() {
document.title ="Home";
useEffect(() => {
getDocs(collection(db, 'listings'))
.then(queryCollection => {
queryCollection.forEach((doc) => {
console.log(doc.id, " => ", doc.data());
const element = <BookListing id="456" ISBN="0101" title="sample_title" image="https://nnpbeta.wustl.edu/img/bookCovers/genericBookCover.jpg" price="25"/>;
ReactDOM.render(
element,
document.getElementById('home-contents-main')
);
})
});
}, []);
return (
<div className="home">
<div className="home_container">
<div id="home-contents-main" className="home_contents">
</div>
</div>
</div>
)
}
export default Home
It's best (and most common) to separate the task into two: asynchronously fetching data (in your case from firestore), and mapping that data to React components which are to be displayed on the screen.
An example:
function Home() {
// A list of objects, each with `id` and `data` fields.
const [listings, setListings] = useState([]) // [] is the initial data.
// 1. Fetching the data
useEffect(() => {
getDocs(collection(db, 'listings'))
.then(queryCollection => {
const docs = [];
queryCollection.forEach((doc) => {
docs.push({
id: doc.id,
data: doc.data()
});
// Update the listings with the new data; this triggers a re-render
setListings(docs);
});
});
}, []);
// 2. Rendering the data
return (
<div className="home">
<div className="home_container">
<div className="home_contents">
{
listings.map(listing => (
<BookListing
id={listing.id}
ISBN={listing.data.ISBN}
title={listing.data.title}
image={listing.data.image}
price={listing.data.price}
/>
))
}
</div>
</div>
</div>
);
}
Some tips:
Fetching data from other web servers or services can be, and typically is, done in the same manner.
This example could be improved a lot in terms of elegance with modern JS syntax, I was trying to keep it simple.
In most cases, you don't want to use ReactDOM directly (only for the entry point of your app), or mess with the DOM manually; React handles this for you!
If you're not familiar with the useState hook, read Using the State Hook on React's documentation. It's important!
You can create a reusable component, and pass the data to it, and iterate over it using map() . define a state, and use it within the useEffect instead of creating elements and handling the process with the state as a data prop.
function BookListing({ id, ISBN, title, image, price }) {
return (
<div className="bookListing">
<div className="bookListing_info">
<p className="bookListing_infoTitle">{title}</p>
<p className="bookListing_infoISBN">
<span className="bookListing_infoISBNtag">ISBN: </span>
{ISBN}
</p>
<p className="bookListing_infoPrice">
<small>$</small>
{price}
</p>
</div>
<img className="bookListing_img" src={image} alt=""></img>
<button className="bookListing_addToCart">Add to Cart</button>
</div>
);
}
function Home() {
const [data, setData] = useState([]);
useEffect(() => {
document.title = 'College Reseller';
getDocs(collection(db, 'listings')).then((queryCollection) => setData(queryCollection));
}, []);
return (
<div className="home">
<div className="home_container">
<div id="home-contents-main" className="home_contents">
{data.map((doc) => (
<BookListing
id="456"
ISBN="0101"
title="sample_title"
image="https://nnpbeta.wustl.edu/img/bookCovers/genericBookCover.jpg"
price="25"
/>
))}
</div>
</div>
</div>
);
}
export default Home;

Reset component into modal

I have a component that is inserted in a modal and that includes a CheckListBox. When the modal starts each time, the component is not reset. How can I do? How Force reset? I use reactjs with hooks.
How can I trigger a reset event every time the modal opens?
Thanks a lot.
const CheckList = ({title, api, color, onChange }) => {
const [items, setItems] = useState([]);
let listCheck = [];
useEffect(() => {
axiosApi.get( `${api}`).then((res)=>{
setItems(res.data);
})
}, [])
function handleClick(ev, item) {
if (ev.target.checked) {
listCheck.push(item)
onChange(listCheck);
}
else
{
listCheck = listCheck.filter(riga => {
return (riga.id !== item.id)});
onChange(listCheck);
}
}
return (
<>
<div class="card rd-card-list">
<div class="card-header">
{title}
</div>
<div class="card-content rd-card-content">
<div class="content rd-scroll">
<ul class="rd-ul">
{ items.map( (item) =>
<li class="rd-li" key={item.id}>
<label class="checkbox">
{item.description}
</label>
<input type="checkbox" onClick={(ev) => handleClick(ev, item)}/>
</li>
)
}
</ul>
</div>
</div>
</div>
</>
);
}
export default CheckList;
in my modal.js
<CheckList title="mytititle" api="/api/users" onChange={(itx) => {
formik.setFieldValue('users', itx)
} }/>
The easiest way is to not render the modal until it's open:
<div>
{modalOpen &&
<Modal open={modalOpen}>
<CheckList title="mytititle" api="/api/users" onChange={(itx) => {
formik.setFieldValue('users', itx)
} }/>
</Modal>
}
</div>
So whenever you close the modal, it will be removed from DOM, along with any data that this component had.
React life cycle events can be used to perform operation before a component can be rendered. 'constructor()' or 'componentDidMount()' can be used in class components to reset the data or any other operation before rendering the component.
Since you are using function component, you can use React hooks to mimic the life cycle events using 'useEffect()'.

react-redux: Rendering a component after an API call

I am building an app which uses user input and shows number of recipes and they can click on recipe card to view ingredients as well. Every time they click on recipe card I make an API call to get appropriate recipe ingredient. But I am not able to figure out how to show the component which contains the recipe ingredients. I tried with conditional routing and conditional rendering as well but couldn't find the solution.
Recipe_Template.js
export class RecipeTemplate extends Component {
renderRecipe = recipeData => {
return recipeData.recipes.map(recipeName => {
return (
<div className="container">
<div className="row">
<div className="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<a
href={recipeName.source_url}
target="_blank"
onClick={() => {
this.props.fetchRecipeId(recipeName.recipe_id);
}}
>
<img
src={recipeName.image_url}
className="mx-auto d-block img-fluid img-thumbnail"
alt={recipeName.title}
/>
<span>
<h3>{recipeName.title}</h3>
</span>
</a>
<span}>
<h3>{recipeName.publisher}</h3>
</span>
</div>
</div>
</div>
);
});
};
render() {
return (
<React.Fragment>
{this.props.recipe.map(this.renderRecipe)}
</React.Fragment>
);
}
}
Recipe_Detail.js
class RecipeDetail extends Component {
renderRecipeDetail(recipeData) {
return recipeData.recipe.ingredients.map(recipeIngredient => {
return <li key={recipeIngredient}>recipeIngredient</li>;
});
}
render() {
if (this.props.recipeId === null) {
return <div>Loading...</div>;
}
return <ul>{this.props.recipeId.map(this.renderRecipeDetail)}</ul>;
}
}
function mapStateToProps({ recipeId }) {
return { recipeId };
}
export default connect(mapStateToProps)(RecipeDetail);
Not entirely sure why you would need Redux here (unless it's being shared among other nested components), but I'm fairly certain you can just utilize React state.
One approach would be to configure your routes as such:
<Route path="/recipes" component={Recipes} />
<Route path="/recipe/:id" component={ShowRecipe} />
When the user sends a query, gets some results, and you display all matching recipes to a Recipes component. Each recipe then has a name (and other associated displayable data) and a clickable link:
<Link to={`/recipe/id?recipeId=${recipeId}`}>View {recipeName} Recipe</Link>
which for simplicity sake might look like:
<ul>
<Link to="/recipe/id?recipeId=08861626">View Prosciutto Bruschetta Recipe</Link>
<Link to="/recipe/id?recipeId=04326743">View Pasta Bundt Loaf Recipe</Link>
...etc
</ul>
When the user clicks on the link, react-router sends the user to the ShowRecipe component with a unique recipeId.
ShowRecipe then makes another AJAX request to get the recipe details:
ShowRecipe.js
export default class ShowRecipe extends Component {
state = { recipeDetail: '' }
componentDidMount = () => {
const { recipeId } = this.props.location.query; // <== only natively available in react-router v3
fetch(`http://someAPI/recipe/id?recipeId=${recipeId}`)
.then(response => response.json())
.then(json => this.setState({ recipeDetail: json }));
}
render = () => (
!this.state.recipeDetails
? <div>Loading...</div>
: <ul>
{this.state.recipeDetail.map(ingredient => (
<li key={ingredient}>ingredient</li>
)}
</ul>
)
}
Another approach:
Have the recipeDetails stored and available within the original fetched recipes JSON. Then map over the recipes and create multiple <Card key={recipeId} recipeName={recipeName} recipeDetail={recipeDetail} /> components for each recipe.
which for simplicity sake might look like:
<div>
{this.state.recipes.map(({recipeId, recipeName, recipeDetail}), => (
<Card key={recipeId} recipeName={recipeName} recipeDetail={recipeDetail} />
)}
</div>
Then each individual Card has it's own state:
Card.js
export default class Card extends Component {
state = { showDetails: '' }
toggleShowDetails = () => this.setState(prevState => ({ showDetails: !this.state.showDetails }))
render = () => (
<div>
<h1>{this.props.recipeName} Recipe</h1>
<button onClick={toggleShowDetails}> {`${!this.state.showDetails ? "Show" : "Hide"} Recipe<button>
{ this.state.showDetails &&
<ul>
{this.props.recipeDetail.map(ingredient => (
<li key={ingredient}>ingredient</li>
)}
</ul>
}
)
}
Therefore, by default the recipeDetail is already there, but hidden. However, when a user clicks the Card's button, it will toggle the Card's showDetails state to true/false to display/hide the recipe detail.

Resources