The addProject function of my code is updating state correctly but the new project is not added to the DOM afterwards. WHY?
What I tried so far:
forceUpdate()
Made a deep copy of the array
Changed the key for the map to be the project title
import React from 'react'
const productBacklog = [
{ id: 1, text: 'FrontEnd'},
{ id: 2, text: 'Finished page - for Cluster'}
];
const parkingLot = [
{ id: 1, text: 'Home page: Google Log In/Github API ---Update: Have Google Cloud account for this'},
{ id: 2, text: 'Screenshots of steps needed to setup test class w/annotation & imports' },
];
const projects = [
{ id: 1, title: 'Parking Lot', children: parkingLot },
{ id: 2, title: 'Product Backlog', children: productBacklog }
];
export default class App extends React.Component {
constructor() {
super();
this.state = {
projects: projects,
}
}
addProject = () => {
const newProject = {
id: Math.round(Math.random() * 1000000),
title: 'newProject',
children: [],
}
const projects = [...this.state.projects];
projects.push(newProject);
this.setState({projects: [...projects]}, () => {
console.log(this.state.projects) // SHOWS THAT STATE IS UPDATED CORRECTLY
})
}
render () {
return (
<div className='App'>
<input type='button' value='Add project' onClick={this.addProject}/>
<div className='projects'>
{projects.map((project, projectId) =>
<div className='Project' key={projectId}>
Project-Title: { project.title}
</div>
)}
</div>
</div>
)
}
}
As I said in the comments, you need to map over this.state.projects. You should carefully name things.. this happened only because you had a global variable called projects (which is synced with the component's state, bad idea in my opinion), otherwise it would've been an obvious error. Also I think functional components are a little bit nicer:
import React, { useState } from 'react';
const productBacklog = [
{ id: 1, text: 'FrontEnd'},
{ id: 2, text: 'Finished page - for Cluster'}
];
const parkingLot = [
{ id: 1, text: 'Home page: Google Log In/Github API ---Update: Have Google Cloud account for this'},
{ id: 2, text: 'Screenshots of steps needed to setup test class w/annotation & imports' },
];
const projects = [
{ id: 1, title: 'Parking Lot', children: parkingLot },
{ id: 2, title: 'Product Backlog', children: productBacklog }
];
export default function App() {
const [state, setState] = useState({projects: projects})
const addProject = () => {
const newProject = {
id: Math.round(Math.random() * 1000000),
title: 'newProject',
children: [],
}
setState({projects: [...state.projects, newProject]})
}
return (
<div className='App'>
<input type='button' value='Add project' onClick={addProject}/>
<div className='projects'>
{state.projects.map((project, projectId) =>
<div className='Project' key={projectId}>
Project-Title: { project.title}
</div>
)}
</div>
</div>
)
}
Comment from Péter Leéh solved the problem: Map over this.state.projects instead of just projects. Thx!
Related
I am assigned a simple task to render permissions of different users using React JS.
There is a little problem and I think the page is not rendered according to the received props data.
Here is my code with little bit explanation.
// my App.js file
import { useState } from "react";
import "./App.css";
import Dropdown from "./components/Dropdown";
import Permissions from "./components/Permissions";
function App() {
const users = [
{
id: 1,
name: "Cirilla",
permissions: ["Post"],
},
{
id: 2,
name: "Michael Scofield",
permissions: ["Post", "Fetch", "Write"],
},
{
id: 3,
name: "Thomas Shellby",
permissions: [],
},
{
id: 4,
name: "Jon Snow",
permissions: ["Fetch", "Post"],
},
];
let [currentUser, setCurrentUser] = useState(users[0]);
const permissions = [
{
id: 1,
name: "Post",
val: currentUser.permissions.includes("Post"),
},
{
id: 2,
name: "Fetch",
val: currentUser.permissions.includes("Fetch"),
},
{
id: 3,
name: "Write",
val: currentUser.permissions.includes("Write"),
},
{
id: 4,
name: "Read",
val: currentUser.permissions.includes("Read"),
},
];
const dropDownChangeHandler = (value) => {
/*this function is executed whenever the dropdown is value is changed. setCurrentUser causes the app function to run again and the array permissions is created again according to the selected user*/
const user = users.find((item) => item.name === value);
setCurrentUser(user);
};
console.log(currentUser);
return (
<div className="container">
<Dropdown list={users} onChange={dropDownChangeHandler} />
<Permissions list={permissions} />
</div>
);
}
export default App;
Here is the permissions.js file
import PermissionsItem from "./PermissionsItem";
const Permissions = (props) => {
return (
<div>
{props.list.map((item) => (
<PermissionsItem key={item.id} item={item} />
))}
</div>
);
};
export default Permissions;
And finally, here is the permissionItem.js file
import React, { useEffect, useState } from "react";
const PermissionsItem = (props) => {
/* const [checkboxValue, setCheckBoxValue] = useState(props.item.val); // here useState does not provide the correct value so I have to use useEffect for this */
useEffect(() => {
setCheckBoxValue(props.item.val);
}, [props.item.val]);
const checkBoxChangeHandler = (event) => {
setCheckBoxValue(event.target.checked);
};
return (
<div className="permission-item">
<label htmlFor={props.item.id} className="perm-li">
{props.item.name}
</label>
<input
id={props.item.id}
type="checkbox"
checked={checkboxValue}
onChange={checkBoxChangeHandler}
/>
</div>
);
};
export default PermissionsItem;
The problem is that when I check any checkbox value of any user (suppose Cirilla), and then select the other user from dropdown (suppose Michael Scofield), the checked permission of first user, Cirilla, is also checked in Michael Scofield's permission. The Micheal's data is displayed correctly in the console but checkboxes are not rendered accordingly.
Please Help I have already wasted my whole week on this :( Any kind of help or suggestion is much appreciated. Thank you in advance !
The problem is that your do check by user in permission. There is no dependecy between the "permission" component and currentUser, so it does not render.
App:
const users = [
{
id: 1,
name: "Cirilla",
permissions: ["Post"],
},
{
id: 2,
name: "Michael Scofield",
permissions: ["Post", "Fetch", "Write"],
}
///...
];
const permissions = [
{
id: 1,
name: "Post"
},
{
id: 2,
name: "Fetch"
}
///...
];
const dropdownChange = (e) => {
setCurrentUser(users.find(x => x.id == e.target.value))
}
return (
<div className="App">
<div className="container">
<select value={currentUser?.id} onChange={dropdownChange} >
{users.map((item) => {
return <option value={item.id}>{item.name}</option>})
}
</select>
<Permissions list={permissions} currentUser={currentUser}/>
</div>
</div>
);
Permission and PermissionItem
const Permissions = (props) => {
return (
<div>
{props.list.map((item) => (
<PermissionsItem
key={item.id}
item={item}
hasItem={props.currentUser.permissions?.includes(item.name)}/>
))}
</div>
);
};
const PermissionsItem = (props) => {
const [checkboxValue, setCheckBoxValue] = useState(false);
useEffect(() => {
setCheckBoxValue(props.hasItem)
},[props.hasItem])
const checkBoxChangeHandler = (event) => {
//TODO users update
setCheckBoxValue(event.target.checked);
};
return (
<div className="permission-item">
<label htmlFor={props.item.id} className="perm-li">
{props.item.name}
</label>
<input
id={props.item.id}
type="checkbox"
checked={checkboxValue}
onChange={checkBoxChangeHandler}
/>
</div>
);
I have 3 elements and I want to add a new element by clicking on any div, but the problem is after adding new elements to the array they don't get rendered out of the component.
import React, { useState } from "react";
import "./styles.css";
export default function App() {
let elements = [
{ id: 0, text: "first" },
{ id: 1, text: "second" },
{ id: 2, text: "third" }
];
const [state, setstate] = useState(elements);
function handleClick() {
elements.push({ id: 3, text: "xxx", checkBox: null });
setstate(elements);
console.log(state); //state shows 4 elememnt but they don't render in
}
return (
<div className="App">
{state.map((e) => (
// why this don't render the new elements?
<div onClick={handleClick}>{e.text}</div>
))}
</div>
);
}
in codesandbox https://codesandbox.io/s/beautiful-silence-c1t1k?file=/src/App.js:0-641
You should not mutate the state directly, it's not a good practice. Instead try as:
function handleClick() {
setstate(prevState => [
...prevState,
{ id: 3, text: "xxx", checkBox: null }
])
}
By doing this you are cloning the previous state of the array and adding that new element into the copy of the array what you can pass to setState function.
See the working CodeSandbox here.
You should not mutate the state directly
import React, { useState } from "react";
import "./styles.css";
const defaultElements = [
{ id: 0, text: "first" },
{ id: 1, text: "second" },
{ id: 2, text: "third" }
];
const newElement = {
id: 3,
text: "xxx",
checkBox: null
};
export default function App() {
const [state, setState] = useState(defaultElements);
function handleClick() {
setState((item) => [...item, newElement]);
}
return (
<div className="App">
{state.map(({ text }, index) => (
<div key={index} onClick={handleClick}>
{text}
</div>
))}
</div>
);
}
I have the following component
import {h, Component} from 'preact'
import {getPersons} from '../../lib/datalayer'
import Person from '../person'
import {SearchInput} from '../search'
export default class Persons extends Component {
state = {
allPersons: [],
persons: [],
search: ''
}
async fetchData () {
try {
const allPersons = await getPersons()
this.setState({allPersons: allPersons.slice(), persons: allPersons.slice()})
} catch (error) {
....
}
}
constructor (props) {
super(props)
this.state = {
allPersons: [],
persons: [],
search: ''
}
this.fetchData()
}
onSearchInput = (search) => {
if (search === '') {
this.setState({search: search, persons: this.state.allPersons.slice()})
} else {
const persons = this.state.allPersons.filter(p => p.name.toLowerCase().includes(search.toLowerCase())).slice()
this.setState({search: search, persons: persons)})
}
}
render () {
const {persons} = this.state
return (
<div>
<SearchInput onInputChange={this.onSearchInput} placeHolder={'filter: name'} />
{persons.map(p => <Person person={p} />)}
</div>
)
}
}
The page renders a list of Persons and it has a filter on top. The filter seems to work fine, I tested it by doing a console.log of the results are just fine
The problem is that, if my list contains the objects:
[{name: 'thomas'}, {name: 'john'}, {name: 'marcus'}, {name: 'usa'}]
And I write in the search input: 'us'
The filter works fine and the result is:
[{name: 'marcus'}, {name: 'usa'}] \\ (the expected result)
In the page this objects are rendered
[{name: 'thomas'}, {name: 'john'}] \\ (wrong, this are the two first elements of the list)
If I search: 'joh'
The filter's result is
[{name: 'john'}] \\ (this is fine)
And the page renders only
[{name: 'thomas'}] \\ (the first element in the list)
It looks like the amount of elements that are rendered it's fine, but the content of the childs of the list is not beeing re-rendered.
Whats's wrong with my code?
React uses keys on the children of a list to determine which items changed and which of them remains the same. Since you have not specified a key on person, it takes index to be the key.
When index is key, you can see how shortening the list to two items, shows up the first two items in the list (the other indices are now missing). To get around this, you have to give a unique identifier on the person as key.
From your object, assuming name is unique (it usually isn't):
{persons.map(p => <Person person={p} key={p.name} />)}
Why are keys necessary - Docs
I cannot reproduce the error with react, did remove some unneeded slice and added unique id to each element (React will complain if you do not give each element a unique key and maybe so will preact).
const Person = React.memo(props => (
<pre>{JSON.stringify(props, undefined, 2)}</pre>
));
class Persons extends React.Component {
state = {
allPersons: [
{ name: 'aaa', id: 1 },
{ name: 'aab', id: 2 },
{ name: 'abb', id: 3 },
{ name: 'bbb', id: 4 },
{ name: 'bbc', id: 5 },
],
persons: [
{ name: 'aaa', id: 1 },
{ name: 'aab', id: 2 },
{ name: 'abb', id: 3 },
{ name: 'bbb', id: 4 },
{ name: 'bbc', id: 5 },
],
search: '',
};
onSearchInput = search => {
if (search === '') {
//slice not needed here
this.setState({
search: search,
persons: this.state.allPersons,
});
} else {
//filter already copies allPersons
const persons = this.state.allPersons.filter(p =>
p.name.toLowerCase().includes(search.toLowerCase())
);
this.setState({ search: search, persons: persons });
}
};
render() {
const { persons } = this.state;
return (
<div>
<input
type="text"
value={this.state.search}
onChange={e => this.onSearchInput(e.target.value)}
placeHolder={'filter: name'}
/>
{persons.map(p => (
<Person person={p} key={p.id} />
))}
</div>
);
}
}
ReactDOM.render(
<Persons />,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I am using react-table and need to create subrows with the data structure below. I have successfully created subrows for each object in the data array. However, each object in the data array contains another array "types."
How would i go about getting each row to list the "type" names as subrows?
My code so far is below:
Table:
import React from 'react';
import ReactTable from 'react-table';
const Table = (props) => {
const subComponent = row => {
return (
<div>
Names of "types" here respectively for each object in data array
(no column headers or anything needed)
</div>
);
};
return (
<ReactTable data={ props.data }
columns={ props.columns }
SubComponent={ subComponent } />
);
};
export default Table;
Data structure:
const data = [
{
id: '12345',
name: 'sports',
types: [
{
name: 'basketball',
id: '1'
},
{
name: 'soccer',
id: '2'
},
{
name: 'baseball',
id: '3'
}
]
},
{
id: '678910',
name: 'food',
types: [
{
name: 'pizza',
id: '4'
},
{
name: 'hamburger',
id: '5'
},
{
name: 'salad',
id: '6'
}
]
}
];
You can rewrite the getSubRows method on useTable optios.
Something like this:
const getSubRows = useCallback((row) => {
return row.types || [];
}, []);
Here is a good example on how to do it https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/archives/v6-examples/react-table-sub-components
From my best guess, your code will look like this:
import React from 'react';
import ReactTable from 'react-table';
const Table = (props) => {
const subComponent = row => {
return (
<div>
row.Original.types.map((type, idx) => (
<div>{{type.id}}</div>
<div>{{type.name}}</div>
))
</div>
);
};
return (
<ReactTable data={ props.data }
columns={ props.columns }
SubComponent={ subComponent } />
);
};
export default Table;
I need some advice on testing functions in terms of how and what
say I have some state.
state = {
categories: [this is full of objects],
products: [this is also full of objects]
}
then I have this function:
filterProducts = () => {
return this.state.products.filter((product => (
product.categories.some((cat) => (
cat.title == this.state.chosenCategory
))
)))
}
this function filters the products array by working out if the products are part of the selected category.
how would you test this?
I've tried this
let productsStub = [
{id: 1, title: 'wine01', showDescription: false},
{id: 2, title: 'wine02', showDescription: false},
{id: 3, title: 'wine03', showDescription: false}
]
wrapper = shallow(<Menu
categories={categoriesStub}
products={productsStub}
/>);
it('should filter products when searched for', () => {
const input = wrapper.find('input');
input.simulate('change', {
target: { value: '01' }
});
expect(productsStub.length).toEqual(1);
});
what this test (I think) is saying, when I search for 01, I expect the product state (well the stub of the state) to filter and return only 1 result. however the test fails and says expected: 1 received: 3 i.e. the filtering isn't working.
I know I could also do wrapper.instance.filterProducts() but again, I'm not very comfortable on function testing in jest.
any advice? would be great to chat it through with someone
thanks
I replicated your problem statement, but not sure how are you maintaining the state model (props/state). But this might help. :)
Checkout the working example here: https://codesandbox.io/s/6zw0krx15k
import React from "react";
export default class Hello extends React.Component {
state = {
categories: [{ id: 1 }],
products: this.props.products,
selectedCat: 1,
filteredProducts: []
};
filterProducts = value => {
let filteredVal = this.props.products.filter(
product => product.id === parseInt(value)
);
this.setState({
filteredProducts: filteredVal
});
};
setValue = e => {
this.setState({
selectedCat: e.target.value
});
this.filterProducts(e.target.value);
};
render() {
return (
<div>
Filter
<input value={this.state.selectedCat} onChange={this.setValue} />
</div>
);
}
}
import { shallow } from "enzyme";
import Filter from "./Filter";
import React from "react";
let productsStub = [
{ id: 1, title: "wine01", showDescription: false },
{ id: 2, title: "wine02", showDescription: false },
{ id: 3, title: "wine03", showDescription: false }
];
let wrapper = shallow(<Filter products={productsStub} />);
it("should filter products when searched for", () => {
const input = wrapper.find("input");
input.simulate("change", {
target: { value: "1" }
});
expect(wrapper.state().filteredProducts.length).toEqual(1);
});