I have state like this:
this.state = {
items = []
}
State is loaded from REST service, which is working correctly.
However later , when user edit one of items in state, I use immutability helper to update state:
const newState = update(this.state, {
items: {
[index]: {$set: newItem}
}
});
this.setState(newState);
This will not be re-drawed (render method will pass , but no visible changes are made on page) and I know why:
Because I am creating array to render like this:
render(){
let itemsrender=[];
for (let i in this.state.items){
let line="";
if (i<this.state.items.length-1) line=<div className="row line"></div>;
atcrender.push(
<div key={this.state.item[i].id}>
<div className="row" >
<ITEM
item={this.state.items[i]}
onUpdate={this.onUpdate}
onDelete={this.onDelete}
/>
</div>
{line}
</div>
)
}
return(
<div>{itemsrender}</div>
);
}
If I can not edit render method of ITEM , is there any solution I can make so item will re-render on update ? Only one that come to my mind is to create wrapping Component.
I think that the issue is with the let itemsrender as this is what gets returned from the render method, this is all that will be rendered. You don't seem to update this anywhere in the loop so the return from the render method never changes.
You could also re-write this by mapping over the state and it might make it a bit easier to see whats going on.
render() {
return (
<div>
{this.state.items.map((item, index) => {
<div key={item.id}>
<div className="row" >
<ITEM
item={item}
onUpdate={this.onUpdate}
onDelete={this.onDelete}
/>
</div>
{index < this.state.items.length - 1 ? <div className="row line"></div> : ''}
</div>
})}
</div>
)
}
}
render(){
let itemsrender=[];
for (let i in this.state.items){
let line="";
if (i<this.state.items.length-1) line=<div className="row line"></div>;
atcrender.push(
<div key={this.state.item[i].id}>
<div className="row" >
<ITEM
item={this.state.items[i]}
onUpdate={this.onUpdate}
onDelete={this.onDelete}
/>
</div>
{line}
</div>
)
}
return(
<div>{itemsrender}</div>
);
}
Should atcrender on line 6, in fact be itemsrender? Or is there code elsewhere in this file that would reveal more about why you've done things this way?
Related
I would like to change a true/false state in a child component and pass it to a parent component, where the state is actually defined. Right now, this results only in an error.
Parent:
const PostTemplate = ({ data }) => {
const [isSlider, setIsSlider] = useState(false);
return (
<Layout class="page">
<main>
<Views setIsSlider={isSlider} {...data} />
</main>
</Layout>
)}
</>
);
};
Child:
const Views = (data) => {
return (
<div
key="views"
className="post__gallery views"
>
{data.views.edges.map(({ node: view }) => (
<divy
onClick={() => setIsSlider(true)}
>
<GatsbyImage
image={view.localFile.childImageSharp.gatsbyImageData}
/>
<div
className="post__caption"
dangerouslySetInnerHTML={{
__html: view.caption,
}}
/>
</div>
))}
</div>
);
};
Right now, this puts out:
setIsSlider is not a function.
(In 'setIsSlider(true)', 'setIsSlider' is false)
Perhaps also relevant the console.log from React:
Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
You're passing isSlider as a prop. You should be passing setIsSlider.
Note that state management in React is a complex topic and you probably want to do some research into how it is done in general. Directly passing a state setting callback works, but it doesn't scale well to complex applications.
Instead of passing the state variable you have to pass the state function like this:
Parent
const PostTemplate = ({ data }) => {
const [isSlider, setIsSlider] = useState(false);
return (
<Layout class="page">
<main>
<Views setIsSlider={setIsSlider} {...data} />
</main>
</Layout>
)}
</>
);
};
Child
You've to use data.<function_passed_via_props> to access the setIsSlider function, like this:
const Views = (data) => {
return (
<div
key="views"
className="post__gallery views"
>
{data.views.edges.map(({ node: view }) => (
<divy
onClick={() => data.setIsSlider(true)}
>
<GatsbyImage
image={view.localFile.childImageSharp.gatsbyImageData}
/>
<div
className="post__caption"
dangerouslySetInnerHTML={{
__html: view.caption,
}}
/>
</div>
))}
</div>
);
};
To Fix: Change setIsSlider={isSlider} into setIsSlider={setIsSlider}.
However, in case you need to manage more than a few states across components. I would suggest using Redux. This can centralize your common-used states, you can access or update these states in any component using Redux.
Right now i am in Home.js page and i want to render Article.js component/page when user click on particular card (Card.js component). Here is my Home.js code
const Home = () => {
const posts = useSelector((state) => state.posts)
const [currentId, setCurrentId] = useState(null)
const handleClick = () => {
return <Article />
}
return (
<div className="container">
<h4 className="page-heading">LATEST</h4>
<div className="card-container">
{
posts.map(post => <Card key={post._id} post={post} setCurrentId={setCurrentId} onClick={handleClick} />)
}
</div>
</div>
)
}
ONE MORE PROBLEM :
How can I send post variable into onClick method? when i send it method is getting called.
Thank You in Advance :)
It sounds like you want to use the React Router? As I take it you want to load the post as its own page?
I should also point out that any function passed to onClick cannot return anything. The only purpose return can serve in an event function is to exit the function early.
I do agree with #Jackson that you might want to to look into React Router. But you don't need it. You can conditionally render the Article component based on the currentId.
A click handler shouldn't return anything. Instead of returning the <Article /> from the onClick callback, you would use onClick to control the currentId state. You can pass a function that sets the currentId to the post id based on the post variable in your map like this: onClick={() => setCurrentId(post._id)}.
The return for your Home component will either render the list of posts or a current post, depending on whether or not you have a currentId or just null.
const Home = () => {
const posts = useSelector((state) => state.posts);
const [currentId, setCurrentId] = useState(null);
return (
<div className="container">
{currentId === null ? (
// content for list of posts - when currentId is null
<>
<h4 className="page-heading">LATEST</h4>
<div className="card-container">
{posts.map((post) => (
<Card
key={post._id}
post={post}
// arrow function takes no arguments but calls `setCurrentId` with this post's id
onClick={() => setCurrentId(post._id)}
/>
))}
</div>
</>
) : (
// content for a single post - when currentId has a value
<>
<div
// setting currentId to null exits the aritcle view
onClick={() => setCurrentId(null)}
>
Back
</div>
<Article
// could pass the whole post
post={posts.find((post) => post._id === currentId)}
// or could just pass the id and `useSelector` in the Article component to select the post from redux
id={currentId}
// can pass a close callback to the component so it can implement its own Back button
onClickBack={() => setCurrentId(null)}
/>
</>
)}
</div>
);
};
To pass in the click hadler the params you want, one could do something like this:
posts.map(post =>
<Card
key={post._id}
post={post}
onClick={() => handleClick(post)} />
)
I am using React to display book titles that I want filtered by category. I want the titles filtered once a checkbox next to the category name is clicked. I am not using a submit button.
I am somewhat new to React and read the documentation about "lifting state," but I haven't been able to get that to work. I have not yet read the Hooks or Context API documentation. Perhaps that's the solution, but it seems what I'm doing isn't complex enough for that...maybe not?
class Checkbox extends Component {
state = {
checked: false
}
handleClick = (e) => {
this.setState(() => ({ checked: !this.state.checked }))
}
render() {
const name = this.props.name;
return (
<label className="form__group">
<input type="checkbox" checked={this.state.checked} onChange={this.handleClick} className="form__input" />
<span className="form__faux-input"></span>
<span className="form__label">{name}</span>
</label>
)
}
}
function Sidebar({ categories }) {
return (
<div className="sidebar">
<div className="controls">
<div className="filter">
<h2 className="filter__heading">Filter By Category</h2>
<form className="filter-form">
{!categories
? <Spinner />
: categories.map((item) => (
<Checkbox key={item} name={item} />
))
}
<div className="form__group">
<button className="btn btn--rectangle btn--green">
<span className="btn-wrapper">Reset</span>
</button>
</div>
</form>
</div>
</div>
</div>
);
}
class App extends Component {
state = {
books: null,
categories: null
}
async componentDidMount() {
const { books, categories } = await getBooks();
this.setState(() => ({
books: books,
categories: categories
}));
}
render() {
const { books } = this.state;
const { categories } = this.state;
return (
<div className="App">
<Header />
<main className="main">
<div className="uiContainer">
<Sidebar
categories={categories}
/>
{!books
? <Spinner />
: <Card books={books} />
}
</div>
</main>
</div>
);
}
}
I dont 100% understand the question, but if you want to make a section like
[x] cats
[x] dogs
[ ] rabbits // dont show rabbits
Then you can keep the selection and the result part in one react element, if you dont understand the 'lifting state up' section
the state should contain an array of objects like this:
[{
allow: true,
title: 'cat'
},
{allow: false, title: 'rabbit'}]
To update the list use something like this:
this.state.map(({title, allow}) => (
<div>
<TickBox onClick={() => this.toggleAnimal(title)} value={allow}/>
<p>{animalName}</p>
</div>
)
toggleAnimal function should find the animal using the title, and update the state
Then you can filter out all the not allowed animals
this.state
.filter(animal => animal.allowed)
.map(a => <p>{a.title}</p>)
lifting state up
At this point you have 1 component, and the render function looks like this:
<h1>Please select the animals</h1>
{
animals.map(_ => <div><tickbox onClick={() => this.handleToggle(title)} /><title></div>)
}
<h1>Here are the filtered animals</h1>
{
animals.filter(a => a.allow).map(animal => animal.title).map(/* to JSX */)
}
It would be prettier and more responsive if the root component would look like this:
render () {
<SelectAnimals toggle={handleToggle} animals={this.state} />
<ShowFilteredAnimals animals={this.state} />
}
handleToggle (title) {
this.setState(...)
}
As so can see, the SelectAnimals gets a function as an argument, it can communicate with it's parent, by calling props.toggle (with the title as argument)
So SelectAnimals would look like this:
props.animals.map(animal => (
<div>
<TickBox onClick=(() => {props.toggle(animal.title)}) /> // HERE
<p>{animal.title}</p>
</div>
))
So when the tick-box fires a click event, it calls an arrow func. that calls props.toggle function with the title
In the parent of SelectAnimals, the parent element binds a handler function to SelectAnimals.toggle like this:
handleToggle (title) { // the child element called this function, it just got copied
}
PS: I made some renames in my code the handleToggle function can be the same as toggleAnimals
The parent component App needs to be able to tell Card what the selected category is, assuming Card is where the list renders.
To do that, you can:
1) create a callback function inside <App>:
_setCurrentCategory(selection) {
this.setState({currentCategory: selection})
}
2) pass it to <Checkbox /> as a prop and use it in an onChange:
class Checkbox extends Component {
render() {
const {name, setCurrentCategoryCallback } = this.props
return (
<label className="form__group">
<input
type="checkbox"
onChange={() => setCurrentCategoryCallback(name)}
className="form__input"
/>
<span className="form__faux-input"></span>
<span className="form__label">{name}</span>
</label>
)
}
}
.. this will change the state in the parent so that you can then
2) then pass the state from <App /> to <Card />:
<Card
currentCategory={this.state.currentCategory}
books={books}
/>
^^ assuming that this is where the filtered list will render. Inside the Card component, you can filter/order then render the list as you please since it now has both the list of books, and the currently selected category.
This is very loosely coded, but hopefully you get the idea!
also, when deconstructing you don't need to do this:
const { books } = this.state;
const { categories } = this.state;
you can instead do this: const { books, categories} = this.state since they are both coming from state :)
so for this component, it just shows a list of cards with some information passed through the props which is a JSON array set into events state. So, since it's a variable number of results each time I created an array of the number of times I want to the component to be rendered which works fine. Each card has its relevant information passed through
this.state.events[i]
Ok all that works fine EXCEPT one spot, which is the FlatButton onClick action.
<FlatButton label="Download ICS" onClick={() => console.log(this.state.events[i].location)} />
If I try access the state with i, then I will get a
TypeError: Cannot read property 'location' of undefined
or whatever property of the JSON I choose.
BUT, if i use an actual manual index like 5, 4, 3, etc then it works just fine. Thing is, I need it to be different for each button, otherwise other event cards have a button that does the wrong thing for that event. Keep in mind again that for all the other times I use i to access the JSON array, it works fine above such as in the CardText component.
Here is a visual:
I don't know, it might have something to do with component life cycle, but I don't know enough to know precisely.
Thanks
class SearchResultsList extends Component {
constructor(props) {
super(props);
this.state = {
events: [],
eventsLength: 0,
};
}
componentDidMount() {
var count = Object.keys(this.props.events).length;
this.setState({ events: this.props.events });
this.setState({ eventsLength: count });
};
render() {
var cardSearchHolder = [];
for(var i = 0; i < this.state.eventsLength; i++) {
cardSearchHolder.push(
(
<div key={i}>
<Card>
key={this.state.events[i]._id}
<CardHeader
title={this.state.events[i].event_name}
subtitle={this.convertDateTime(this.state.events[i].start_date, this.state.events[i].end_date)}
actAsExpander={true}
showExpandableButton={true}
/>
<CardText expandable={true}>
<div>
Category: {this.state.events[i].event_category}
</div>
<div>
Where: {this.state.events[i].location}
</div>
<div id="miniSpace"></div>
<div>
Description: {this.state.events[i].event_description}
</div>
</CardText>
<CardActions>
<FlatButton label="Download ICS" onClick={() => console.log(this.state.events[i].location)} />
</CardActions>
</Card>
<div id="space"></div>
</div>)
);
}
return(
<div>
<MuiThemeProvider>
<Card>
{cardSearchHolder}
</Card>
</MuiThemeProvider>
<div id="miniSpace"></div>
</div>
);
}
}
I'm super new to react but excited about its potential. Still getting to grips with the fundamentals of it all so any explanation would be greatly appreciated.
I'm looking to render an 'About' component as the user clicks a button in the 'Nav' component (with the aim to toggle this later down the line).
I've attempted to do it in the simplest way I can think of, but this is obviously very wrong:
class Nav extends React.Component {
renderAbout() {
return (
<About />
);
}
render() {
return (
<div className="Nav">
<div className="Button-Container">
<div className="Nav-Text About-Button">
<h2 onClick={() => this.renderAbout()}>About</h2>
</div>
</div>
</div>
);
}
}
Would this have something to do with updating the 'state' of the About component?
Thanks in advance.
You can use state to define if imported component About has to be rendered or not.
class Nav extends React.Component {
state = {
isAboutVisible: false,
}
render() {
return (
<div className="Nav">
<div className="Button-Container">
<div className="Nav-Text About-Button">
<h2 onClick={() => this.setState({ isAboutVisible: true }) }>About</h2>
</div>
</div>
{ this.state.isAboutVisible ? <About /> : null }
</div>
);
}
}
You currently do not have "About" component in actual view, you just render it somewhere out there, in the void!
To properly render a component you have to specify its place in JSX expression. Also, as one of the easiest solutions) you probably want to toggle it. So that translates to something like this:
constructor(props){
super(props)
this.state={toggle:false}
}
renderAbout(toggle) {
if(toggle)
return <About />
else return null;
}
render() {
return (
<div className="Nav">
<div className="Button-Container">
<div className="Nav-Text About-Button">
<h2 onClick={() => this.setState({toggle: !toggle})}>About</h2>
</div>
</div>
{this.renderAbout(this.state.toggle)}
</div>
);
}
}
Yes, you have to change state of the component. Changing the state will automatically rerender your component. In your example it should be something like:
class Nav extends React.Component {
state = {
showAbout: false; // initial state
}
renderAbout = () => {
if (!this.state.showAbout) return '';
return (
<About />
);
}
// ES6 priavte method syntax
handleButtonClick = () => {
this.setState({showAbout: true});
}
render() {
return (
<div className="Nav">
<div className="Button-Container">
<div className="Nav-Text About-Button">
<h2 onClick={this.handleBtnClick}>About</h2>
{this.renderAbout()}
</div>
</div>
</div>
);
}
}
You could also consider using for example this package: https://www.npmjs.com/package/react-conditions
Also, remember that there is a rule that each method which listen for an event should start with the "handle" word. Like in may example.