dynamically generate react components from data from an object as props - javascript

Context: trying to build a simple todo list app in react and typescript for educational purposes.
What I'm trying to do: Render as many ProjectMenuItem components as there are individual projects in my projects object, using the title of the project as props to pass into the components. "Project 1, Project 2, Project 3" should render to the screen as p elements.
What happens instead: The app does not compile and I get an error saying: Uncaught Error: Objects are not valid as a React child (found: object with keys {projectName}). If you meant to render a collection of children, use an array instead.
ProjectMenuItem component:
export function ProjectMenuItem(projectName: any){
return(
<p>{projectName}</p>
);
}
Parent Component:
export function ProjectList(){
const projects = {
project1: {
title: 'Project 1',
},
project2: {
title: 'Project 2',
},
project3: {
title: 'Project 3',
},
};
function generateProjectMenuItems() {
const projectMenuItems = [];
for (const project in projects) {
const projectName: string = projects[project as keyof typeof projects].title;
projectMenuItems.push(<ProjectMenuItem projectName={projectName} />);
}
return projectMenuItems;
}
return(
<div className="project-list flexbox">
<p>project components go here</p>
{generateProjectMenuItems()}
</div>
)
}
Have tried: I know it is saying I should use it as an array, so I tried mapping the projectMenuItems array to an array of span elements containing the components, but this results in the same error message.
return projectMenuItems.map(el => <span className="project-span" key={Math.random().toString()}>{el}</span>);
TL;DR I am trying to generate components for each object in a larger object and I'm not sure what I'm doing wrong. Very new to react.

Your ProjectMenuItem component isn't quite right. You want to accept the props as a (destructured) object:
export function ProjectMenuItem({ projectName }: { projectName: string }) {
As it is, React is providing it with the entire props object, which it receives as projectName. When it tries to render this in <p>{projectName}</p> it is giving you the error "Objects are not valid".

So this is probably not best practices, but this is how I ended up solving the problem yesterday. I am very open to feedback on things I could do better. I wrote it this way because it was the only way I could have my code work when it loaded from an empty (null) state when the app renders at first, because trying to run Object.keys() or map() on things that were null was making my app crash, so I wrapped my logic in a hook and conditional rendering to make things work.
Parent Component:
import React, {useContext} from "react";
import { AllContext } from "../App";
import ProjectMenuItem from "./ProjectMenuItem";
export default function ProjectList(){
//use context to pass state from root component
const {allProjects, selectedProject, currentTask} = React.useContext(AllContext);
const [allProjectsCopy, setAllProjects] = allProjects;
//make state for conditional rendering
const[displayProjects, setDisplayProjects] = React.useState<any>(null);
let projects: any;
//run this only when state updates
React.useEffect(() => {
console.log('Hello from the Project List component!', allProjectsCopy);
if(allProjectsCopy !== null){
console.log('allProjectsCopy updated and is NOT null.');
projects = Object.keys(allProjectsCopy);
let displayProjectsUpdate = projects.map((item: any)=>(
<div key={item}>
<ProjectMenuItem projectName={item} />
</div>
)
);
setDisplayProjects(displayProjectsUpdate);
}
}, [allProjectsCopy]);
return(
<div className="project-list flexbox">
<p>project components go here</p>
{(displayProjects !== null) ? displayProjects : null}
</div>
)
}
Child Components:
export default function ProjectMenuItem(props: any) {
return(
<div className="project-menu-item flexbox">
<img src="https://img.icons8.com/material-two-tone/48/null/overview-pages-3.png" alt="icon" role="button" tabIndex={0} aria-label="Clickable image" />
<h3 className="project-item-heading">{props.projectName}</h3>
<img src="https://img.icons8.com/ios/50/null/edit-property.png" alt="icon" role="button" tabIndex={0} aria-label="Clickable image"/>
</div>
);
}

Related

How to remove an element from a Mobx observable array, without causing the entire consuming component to rerender?

So let's say i have a todoStore. It has an action that deletes a todo by id. Note that i tried both filter and splice:
export default class TodosStore {
constructor() {
makeAutoObservable(this)
}
todos = [
{
id: 1,
name: "name1",
completed: true
},
{
id: 15,
name: "name2",
completed: true
},
{
id: 14,
name: "name3",
completed: true
}
]
removeTodo(id) {
// this.todos = this.todos.filter(todo=>todo.id != id)
for (let todo of this.todos) {
if (todo.id == id) {
const indexOf = this.todos.indexOf(todo)
this.todos.splice(indexOf, 1)
}
}
}
};
The consuming Todos component(Note that i'm wrapping the Todo with observer):
import { combinedStores } from "."
const ObservableTodo = observer(Todo);
export default observer(() => {
const { todosStore } = combinedStores
return (
<div >
{todosStore.todos.map(todo=>{
return(
<ObservableTodo onDelete={()=>{todosStore.removeTodo(todo.id)}} onNameChange={(value)=>{todosStore.editTodoName(todo.id,value)}} key={todo.id} todo={todo}></ObservableTodo>
)
})}
</div>
)
})
The simple Todo component:
export default ({todo,onNameChange,onDelete}) => {
return (
<div style={{padding:'10px',margin:'10px'}}>
<p>ID: {todo.id}</p>
<input onChange={(e)=>{onNameChange(e.target.value)}} value={todo.name}></input>
<p>Completed: {todo.completed ? 'true' : 'false'} <button onClick={onDelete} className="btn btn-danger">Delete</button></p>
</div>
)
}
Even though i'm clearly mutating(as opposed to constructing a new array) the todos array within the store, Todos component rerenders(i see it via console.logs),
and so does every remaining Todo component.
Is there any way around it? Is there anything wrong with my setup perhaps? I'm using latest Mobx(6) and mobx-react.
Todos component is supposed to rerender because it depends on todos array content (because it map's over it). So when you change todos content by adding or removing some todo - Todos component will rerender because it needs to render new content, new list of todos.
Each single Todo rerenders because you have not wrapped it with observer. It is a good practice to wrap every component which uses some observable state, and Todo is clearly the one that does.
You change the length of the todo array, so the map function kicks in. Then while you are iterating over the elements, you are passing new properties to the ObservableTodo component (onDelete, onChange) this will make the ObservableTodo always rerender.
Even though the component is a Mobx observable, it still follows the "React rules", and when react sees new references in component properties, it will render the component.

React - setting one item from parent's state as child's state

I am a React newbie and trying to learn it by building a simple quote generator where a quote is generated depending on the mood a user selects. The App component holds the state: quotes and moods (where each element is a nested object) and its children are Mood components.
Now, the state of the App component consists of four moods and what I would like to happen is: when a user clicks a button inside the Mood component, s/he is redirected to that mood's page and the Mood component's state is set to that particular mood.
The solution I worked out by myself is very crude and I'm looking for a way to make it more elegant/functional.
Here is the moods object that is the App's state:
const moods = {
mood1: {
type: 'upset',
image: 'abc.png',
},
mood2: {
type: 'unmotivated',
image: 'abc.png',
},
mood3: {
type: 'anxious',
image: 'abc.png',
},
}
the App component:
state ={
moods: moods,
}
render(){
return (
<div className="Container">
<ul className='moods'>
{
Object.keys(this.state.moods).map(key => <Mood
moodsData = {this.state.moods}
key={key}
indexKey = {key}
index={this.state.moods[key].type}
details={this.state.moods[key]}
/>)
}
</ul>
</div>
);}}
And this is how far I got inside the Mood component, where the onClick function on the button is:
handleClick = (e) => {
this.setState({moods: e.target.value});
}
I will be grateful for any pointers/suggestions! Spent so many hours on this I feel like my brain doesn't accept any more Youtube tutorials/Medium articles.
Well, the first thing i notice is that you are trying to use map on a javascript object instead of an array, this could bring some problems when using some functions, so i advice to make it an array.
If you just have one Mood component and based on the mood type it receives change its style, it doesn't actually need to manage state from inside, you can just pass the props to the Mood component and work around what props receives.
For example:
Moods as an array:
const moods = [
{
type: 'upset',
image: 'abc.png',
},
{
type: 'unmotivated',
image: 'abc.png',
},
{
type: 'anxious',
image: 'abc.png',
},
]
i'm assuming you get the Mood list from a server or an external source so that's why i'm keeping moods in state instead of just mapping through the const moods.
state ={
moods: moods,
mood:null,
}
onClick= (key) =>{
console.log(this.state.moods[key]);
this.setState({
mood:this.state.moods[key],
})
}
render(){
return (
<div className="Container">
<ul className='moods'>
{
Object.keys(this.state.moods).map((key) => <div key={key}>
<a onClick={() =>{this.onClick(key)}}>Click here to change the mood to {this.state.moods[key].type}</a>
</div>)
}
</ul>
{this.state.mood ? <Mood actualMood={this.state.mood}/> : null}
</div>
);
}
and the Mood component just gets some props and display something based on what it gets:
class Mood extends Component
{
render()
{
console.log(this.props.actualMood.type);
return (
<div>
<p>You're looking at {this.props.actualMood.type} mood</p>
</div>
);
}
}
this can be easily achieved using react hooks but class based components need a tricky solution and may not look easy at first glance.
If what you want to achieve is to move to another component, you can have a parent Component which only manages the state, then based on a condition (if mood has been selected) render a component either ChooseAMoodComponent or MoodComponent.
Another way to achieve this is through React Router which you can pass some params via URL get params.
but the best practice should be using Redux.

Why is my React Component Not Rendering HTML, but logs correctly?

I'm using Redux to store some API-called data, and in my attempt to render it to screen, I've encountered some issues. Basically, I'm using Redux and Fetch to store all the pulled Products from the Stripe API into a state(prop?) called "products", it's structure looks like this:
{ products:
product1ID: {
items: [
0: {json data from Stripe Product API}
]
},
product2ID: {
items: [
0: {json data from Stripe Product API}
]
},
etc...
}
This is not the most effective structure, and will likely need to be refactored. However, that's not the issue at the moment, because I can, at least, return the data using console.log(). The real issue is, when an element is inside the .forEach / .map loop, I cannot render my component, or any HTML to the page. Here's my "ProductList" component, that breaks down the structure above:
import React from 'react';
import { connect } from 'react-redux';
import ProductListItem from './Items/ProductListItem'
import selectProducts from '../../../_selectors/products.selector';
const ProductList = (props) => (
<div>
<h1>Stripe Product List</h1> //This Prints to Screen
{
Object.entries(props.products).forEach(element => {
return element[1].items.map((product) => {
//console.log(product) <-- this logs the json API data correctly! However, nothing prints to screen using HTML elements below!
return <ProductListItem key={product.id} {...product} /> //This does not display!!! Why?
})
})
}
</div>
);
const mapStateToProps = (state) => {
return {
products: selectProducts(state.productsById)
};
};
export default connect(mapStateToProps)(ProductList);
And heres the ProductListItem, just in case you need it:
import React from 'react';
import { Link } from 'react-router-dom';
const ProductListItem = ({ id, name, metadata }) => (
<Link to={`/memberships/${id}`} className="membership red">
<div className="membership__price">
<span className="membership__price_price-label">${metadata.price_start}</span>
<span className="membership__price_mo-label">/mo</span>
</div>
<div className="membership__info">
<h2>{name}</h2>
<p>{metadata.service_description}</p>
</div>
</Link>
);
export default ProductListItem;
So what gives? It logs the correct data, but won't output any HTML. I can't even get a <p> tag to return when I replace the return <ProductListItem key={product.id} {...product} /> line with something simple, like: return <p>test</p>.
I'm fairly new to React and Redux. Any help is appreciated.
Within JSX you are attempting to return an array of React elements using the Array#forEach method.
The difference between Array#forEach and something like Array#map is the returning value.
The Array.forEach() method does not return a value, disregarding the values returned within its callback.
The Array.map() method returns an array with the values returned from its callback.
Solution
To fix your issue, you should replace Array#forEach with Array#map:
Object.entries(props.products).forEach
with the following:
Object.entries(props.products).map

Why isn't `useContext` re-rendering my component?

As per the docs:
When the nearest <MyContext.Provider> above the component updates, this Hook will trigger a rerender with the latest context value passed to that MyContext provider. Even if an ancestor uses React.memo or shouldComponentUpdate, a rerender will still happen starting at the component itself using useContext.
...
A component calling useContext will always re-render when the context value changes.
In my Gatsby JS project I define my Context as such:
Context.js
import React from "react"
const defaultContextValue = {
data: {
filterBy: 'year',
isOptionClicked: false,
filterValue: ''
},
set: () => {},
}
const Context = React.createContext(defaultContextValue)
class ContextProviderComponent extends React.Component {
constructor() {
super()
this.setData = this.setData.bind(this)
this.state = {
...defaultContextValue,
set: this.setData,
}
}
setData(newData) {
this.setState(state => ({
data: {
...state.data,
...newData,
},
}))
}
render() {
return <Context.Provider value={this.state}>{this.props.children}</Context.Provider>
}
}
export { Context as default, ContextProviderComponent }
In a layout.js file that wraps around several components I place the context provider:
Layout.js:
import React from 'react'
import { ContextProviderComponent } from '../../context'
const Layout = ({children}) => {
return(
<React.Fragment>
<ContextProviderComponent>
{children}
</ContextProviderComponent>
</React.Fragment>
)
}
And in the component that I wish to consume the context in:
import React, { useContext } from 'react'
import Context from '../../../context'
const Visuals = () => {
const filterByYear = 'year'
const filterByTheme = 'theme'
const value = useContext(Context)
const { filterBy, isOptionClicked, filterValue } = value.data
const data = <<returns some data from backend>>
const works = filterBy === filterByYear ?
data.nodes.filter(node => node.year === filterValue)
:
data.nodes.filter(node => node.category === filterValue)
return (
<Layout noFooter="true">
<Context.Consumer>
{({ data, set }) => (
<div onClick={() => set( { filterBy: 'theme' })}>
{ data.filterBy === filterByYear ? <h1>Year</h1> : <h1>Theme</h1> }
</div>
)
</Context.Consumer>
</Layout>
)
Context.Consumer works properly in that it successfully updates and reflects changes to the context. However as seen in the code, I would like to have access to updated context values in other parts of the component i.e outside the return function where Context.Consumer is used exclusively. I assumed using the useContext hook would help with this as my component would be re-rendered with new values from context every time the div is clicked - however this is not the case. Any help figuring out why this is would be appreciated.
TL;DR: <Context.Consumer> updates and reflects changes to the context from child component, useContext does not although the component needs it to.
UPDATE:
I have now figured out that useContext will read from the default context value passed to createContext and will essentially operate independently of Context.Provider. That is what is happening here, Context.Provider includes a method that modifies state whereas the default context value does not. My challenge now is figuring out a way to include a function in the default context value that can modify other properties of that value. As it stands:
const defaultContextValue = {
data: {
filterBy: 'year',
isOptionClicked: false,
filterValue: ''
},
set: () => {}
}
set is an empty function which is defined in the ContextProviderComponent (see above). How can I (if possible) define it directly in the context value so that:
const defaultContextValue = {
data: {
filterBy: 'year',
isOptionClicked: false,
filterValue: ''
},
test: 'hi',
set: (newData) => {
//directly modify defaultContextValue.data with newData
}
}
There is no need for you to use both <Context.Consumer> and the useContext hook.
By using the useContext hook you are getting access to the value stored in Context.
Regarding your specific example, a better way to consume the Context within your Visuals component would be as follows:
import React, { useContext } from "react";
import Context from "./context";
const Visuals = () => {
const filterByYear = "year";
const filterByTheme = "theme";
const { data, set } = useContext(Context);
const { filterBy, isOptionClicked, filterValue } = data;
const works =
filterBy === filterByYear
? "filter nodes by year"
: "filter nodes by theme";
return (
<div noFooter="true">
<div>
{data.filterBy === filterByYear ? <h1>Year</h1> : <h1>Theme</h1>}
the value for the 'works' variable is: {works}
<button onClick={() => set({ filterBy: "theme" })}>
Filter by theme
</button>
<button onClick={() => set({ filterBy: "year" })}>
Filter by year
</button>
</div>
</div>
);
};
export default Visuals;
Also, it seems that you are not using the works variable in your component which could be another reason for you not getting the desired results.
You can view a working example with the above implementation of useContext that is somewhat similar to your example in this sandbox
hope this helps.
Problem was embarrassingly simple - <Visuals> was higher up in the component tree than <Layout was for some reason I'm still trying to work out. Marking Itai's answer as correct because it came closest to figuring things out giving the circumstances
In addition to the solution cited by Itai, I believe my problem can help other people here
In my case I found something that had already happened to me, but that now presented itself with this other symptom, of not re-rendering the views that depend on a state stored in a context.
This is because there is a difference in dates between the host and the device. Explained here: https://github.com/facebook/react-native/issues/27008#issuecomment-592048282
And that has to do with the other symptom that I found earlier: https://stackoverflow.com/a/63800388/10947848
To solve this problem, just follow the steps in the first link, or if you find it necessary to just disable the debug mode

ReactJS - Infinite Loop calling Wrapped Method

I have the usual problem with infinite loop and I don't know why.
Im using reactJS 16.5.2
The loops generally occurs when you write a SetState where not allowed (for example in render method).
Im following this guide: https://medium.com/#baphemot/understanding-reactjs-component-life-cycle-823a640b3e8d
to pay attention about this issue.
I made several HOC(Decorators/Wrapper) components to concentrate general purpose methods in one point using props to propagate them to every children.
It generally works perfectly.
I tried to simplify my components structure below.
The problem is the FORM and its children.
One of the input has a DropDown that has to be populated with a method of the upper Wrapper. I put the call in componentDidMount(as the link above suggest). Unfortunally the wrapper setState seems to trigger a complete descrution and re-building of FORM Component. I put a console.log in every constructor from Wrapped to the form. Only the FORM and all its INPUTS are reacreated (and not updated).
This recreation generates an infinite loop because componentDidMountis triggered everytime.
I don't know how to fix this. I've checked every "key" properties and ALL components has their unique keys. I'm asking you WHY react recreate instead of update?
Is due to the form building method in parent render? And if so, which is the right design pattern to build a form with Async data population?
Simplify your life and instead of creating a bunch of wrappers, just create a single container-component that'll function the same way. For example, you would create a container that cares about data and state, then shares it and its methods with a reusable child component (as shown below, both function the same).
This would work exactly the same way with data fetched from an API. You'll retrieve data in componentDidMount, set it state, then pass down the state to the reuseable component.
You can get super granular with your reusable components. For example a reusable button that's sole purpose is to submit a form. Or a reusable input that only captures numbers between 1 and 100 and so on.
If your components are heavily nested, then consider using redux.
Working example: https://codesandbox.io/s/x2ol8wmzrp
containers/Form.js (container-component)
import React, { Component } from "react";
import Fields from "../components/Fields";
export default class Form extends Component {
state = {
buttonFields: [
{ id: "Apples", quantity: 1 },
{ id: "Strawberries", quantity: 1 },
{ id: "Grapes", quantity: 1 },
{ id: "Apricots", quantity: 1 }
]
};
handleButtonClick = id => {
this.setState(prevState => ({
buttonFields: prevState.buttonFields.map(
item =>
id === item.id ? { id, quantity: item.quantity + 1 } : { ...item }
)
}));
};
render = () => (
<Fields
{...this.state}
onButtonClick={this.handleButtonClick}
title="Container Component"
/>
);
}
components/Fields.js (reusable component)
import React from "react";
export default ({ buttonFields, onButtonClick, title }) => (
<div className="container">
<h1 style={{ textAlign: "center" }}>{title}</h1>
{buttonFields.map(({ id, quantity }) => (
<button
style={{ marginRight: 10 }}
className="uk-button uk-button-primary"
key={id}
onClick={() => onButtonClick(id)}
>
{id} ({quantity})
</button>
))}
</div>
);
containers/Wrapper.js (unnecessary wrapper)
import React, { Component } from "react";
export default WrappedComponent => {
class Form extends Component {
state = {
buttonFields: [
{ id: "Apples", quantity: 1 },
{ id: "Strawberries", quantity: 1 },
{ id: "Grapes", quantity: 1 },
{ id: "Apricots", quantity: 1 }
]
};
handleButtonClick = id => {
this.setState(prevState => ({
buttonFields: prevState.buttonFields.map(
item =>
id === item.id ? { id, quantity: item.quantity + 1 } : { ...item }
)
}));
};
render = () => (
<WrappedComponent
{...this.state}
onButtonClick={this.handleButtonClick}
title="Wrapper"
/>
);
}
return Form;
};
Thanking Matt Carlotta for his answer, I figure out what was the problem.
In the image above I simplified too much so I missed one important declaration.
In "FinalComponent" when I was creating the SomeFormComponent, due to its wrapping, I was doing something like this:
renderForm()
{
var WrappedFormComponent = FormHOC(SomeFormComponent();
return <WrappedFormComponent {...this.props} [...] />
}
It's obvious that with that syntax, the Form is instantatied every time due to renderForm method called in render method.
The solution is very simple. I moved that line above the component:
const WrappedFormComponent = FormHOC(SomeFormComponent();
export default class FinalComponent extends React.Component

Categories

Resources