I have a main component defined as App.js
import "./styles.css";
import { Component } from "react";
import Item from "./components/Item";
class App extends Component {
constructor(props) {
super(props);
this.state = { textInput: "", items: [] };
}
insertItem() {
if (this.state.textInput !== "") {
this.setState((state) => {
const list = state.items.push(state.textInput);
return {
items: list,
textInput: ""
};
});
}
}
deleteItem(index) {
this.setState((state) => {
const list = [...state.items];
list.splice(index, 1);
return {
items: list,
textInput: ""
};
});
}
handleChange(event) {
this.setState({ textInput: event.target.value });
}
render() {
const template = (
<div>
<div>
<input
type="text"
value={this.state.textInput}
onChange={(e) => this.handleChange(e)}
/>
<button onClick={this.insertItem.bind(this)}>Add</button>
</div>
<div>
{this.state.items.map((item, idx) => {
return <Item name={item} removeItem={this.deleteItem.bind(this, idx)} />;
})}
</div>
</div>
);
return template;
}
}
export default App;
and a child Component defined in Item.js
import { Component } from "react";
class Item extends Component {
render() {
return (
<div>
<span>{this.props.name}</span>
<span onClick={this.props.removeItem}>X</span>
</div>
);
}
}
export default Item;
Now my UI looks like
In the above code(App.js) Iam trying to iterate the items and then display the names using the child component. But due to some reason, on entering the text in the input and clicking add its not showing up. Also there are no errors in the console.
Please help, thanks in advance
Edited:
After the recent changes I get this error
You do not need to call this.deleteItem(idx) while passing it to the child.
import "./styles.css";
import { Component } from "react";
import Item from "./components/Item";
class App extends Component {
constructor(props) {
super(props);
this.state = { textInput: "", items: [] };
}
insertItem() {
if (this.state.textInput !== "") {
this.setState((state) => {
const list = state.items.push(state.textInput);
return {
items: list,
textInput: ""
};
});
}
}
deleteItem(index) {
this.setState((state) => {
const list = state.items.splice(index, 1);
return {
items: list,
textInput: ""
};
});
}
handleChange(event) {
this.setState({ textInput: event.target.value });
}
render() {
const template = (
<div>
<div>
<input
type="text"
value={this.state.textInput}
onChange={(e) => this.handleChange(e)}
/>
<button onClick={this.insertItem.bind(this)}>Add</button>
</div>
<div>
{this.state.items.map((item, idx) => {
return <Item name={item} removeItem={this.deleteItem.bind(this, idx)} />;
})}
</div>
</div>
);
return template;
}
}
export default App;
Updating state in react requires a new reference to objects.You're using Array#push. It will not detect your new change and the DOM will not update. You need to return a new reference.
insertItem() {
if (this.textInput === "") {
this.setState((state) => {
// const list = state.items.push(state.textInput);
const list = [...state.items, state.textInput];
return {
list,
textInput: ""
};
});
}
}
In order to track the array, you must add the key attribute:
{this.state.items.map((item, idx) => {
return <Item key={idx} name={item} removeItem={this.deleteItem(idx)} />;
})}
Here I used the index, but it would be better to use some ID of your model.
UPDATE:
I'd move the handler binding in the constructor:
constructor(props) {
super(props);
this.state = { textInput: "", items: [] };
this.insertItem = this.insertItem.bind(this);
}
Then:
<button onClick={this.insertItem}>Add</button>
UPDATE 2:
Okay, it seems you have several mistakes (and I didn't notice them at first glance).
Here is the complete working source (tested):
class App extends Component {
constructor(props) {
super(props);
this.state = { textInput: "", items: [] };
}
insertItem() {
if (this.state.textInput !== "") {
this.setState((state) => {
//const list = state.items.push(state.textInput);
const list = [...state.items, state.textInput];
return {
items: list,
textInput: ""
};
});
}
}
deleteItem(index) {
this.setState((state) => {
const list = [...state.items];
list.splice(index, 1);
return {
items: list,
textInput: ""
};
});
}
handleChange(event) {
this.setState({ textInput: event.target.value });
}
render() {
const template = (
<div>
<div>
<input
type="text"
value={this.state.textInput}
onChange={(e) => this.handleChange(e)}
/>
<button onClick={this.insertItem.bind(this)}>Add</button>
</div>
<div>
{this.state.items.map((item, idx) => {
return <Item key={idx} name={item} removeItem={this.deleteItem.bind(this, idx)} />;
})}
</div>
</div>
);
return template;
}
}
export default App;
I think you should move display of template into return statement rather than inside render
...
render() {
return (
<div>
<div>
<input
type="text"
value={this.state.textInput}
onChange={(e) => this.handleChange(e)}
/>
<button onClick={this.insertItem.bind(this)}>Add</button>
</div>
<div>
{this.state.items.map((item, idx) => {
return <Item name={item} removeItem={this.deleteItem.bind(this, idx)} />;
})}
</div>
</div>
);
}
Related
I am tickling with Algolia autocomplete, and I am trying to replicate their custom renderer in react using the class component. This is the sandbox of the minimal demo of custom renderer using functional component,
and here is my attempt to convert it into a class component.
import { createAutocomplete } from "#algolia/autocomplete-core";
import { getAlgoliaResults } from "#algolia/autocomplete-preset-algolia";
import algoliasearch from "algoliasearch/lite";
import React from "react";
const searchClient = algoliasearch(
"latency",
"6be0576ff61c053d5f9a3225e2a90f76"
);
// let autocomplete;
class AutocompleteClass extends React.PureComponent {
constructor(props) {
super(props);
this.inputRef = React.createRef();
this.autocomplete = null;
this.state = {
autocompleteState: {},
};
}
componentDidMount() {
if (!this.inputRef.current) {
return undefined;
}
this.autocomplete = createAutocomplete({
onStateChange({ state }) {
// (2) Synchronize the Autocomplete state with the React state.
this.setState({ autocompleteState: state });
},
getSources() {
return [
{
sourceId: "products",
getItems({ query }) {
return getAlgoliaResults({
searchClient,
queries: [
{
indexName: "instant_search",
query,
params: {
hitsPerPage: 5,
highlightPreTag: "<mark>",
highlightPostTag: "</mark>",
},
},
],
});
},
getItemUrl({ item }) {
return item.url;
},
},
];
},
});
}
render() {
const { autocompleteState } = this.state;
return (
<div className="aa-Autocomplete" {...this.autocomplete?.getRootProps({})}>
<form
className="aa-Form"
{...this.autocomplete?.getFormProps({
inputElement: this.inputRef.current,
})}
>
<div className="aa-InputWrapperPrefix">
<label
className="aa-Label"
{...this.autocomplete?.getLabelProps({})}
>
Search
</label>
</div>
<div className="aa-InputWrapper">
<input
className="aa-Input"
ref={this.inputRef}
{...this.autocomplete?.getInputProps({})}
/>componentDidUpdate()
</div>
</form>
<div className="aa-Panel" {...this.autocomplete?.getPanelProps({})}>
{autocompleteState.isOpen &&
autocompleteState.collections.map((collection, index) => {
const { source, items } = collection;
return (
<div key={`source-${index}`} className="aa-Source">
{items.length > 0 && (
<ul
className="aa-List"
{...this.autocomplete?.getListProps()}
>
{items.map((item) => (
<li
key={item.objectID}
className="aa-Item"
{...this.autocomplete?.getItemProps({
item,
source,
})}
>
{item.name}
</li>
))}
</ul>
)}
</div>
);
})}
</div>
</div>
);
}
}
export default AutocompleteClass;
and the sandbox of the same version, I also tried using componentDidUpdate() but no luck, any lead where I did wrong would be much appreciated thank you :)
Ok, dont know why you need it made into class component but here you go:
import { createAutocomplete } from "#algolia/autocomplete-core";
import { getAlgoliaResults } from "#algolia/autocomplete-preset-algolia";
import algoliasearch from "algoliasearch/lite";
import React from "react";
const searchClient = algoliasearch(
"latency",
"6be0576ff61c053d5f9a3225e2a90f76"
);
// let autocomplete;
class AutocompleteClass extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
autocompleteState: {},
query: '',
};
this.autocomplete = createAutocomplete({
onStateChange: this.onChange,
getSources() {
return [
{
sourceId: "products",
getItems({ query }) {
console.log('getting query', query)
return getAlgoliaResults({
searchClient,
queries: [
{
indexName: "instant_search",
query,
params: {
hitsPerPage: 5,
highlightPreTag: "<mark>",
highlightPostTag: "</mark>"
}
}
]
});
},
getItemUrl({ item }) {
return item.url;
}
}
];
}
});
}
onChange = ({ state }) => {
console.log(state)
this.setState({ autocompleteState: state, query: state.query });
}
render() {
const { autocompleteState } = this.state;
return (
<div className="aa-Autocomplete" {...this.autocomplete?.getRootProps({})}>
<form
className="aa-Form"
{...this.autocomplete?.getFormProps({
inputElement: this.state.query
})}
>
<div className="aa-InputWrapperPrefix">
<label
className="aa-Label"
{...this.autocomplete?.getLabelProps({})}
>
Search
</label>
</div>
<div className="aa-InputWrapper">
<input
className="aa-Input"
value={this.state.query}
{...this.autocomplete?.getInputProps({})}
/>
</div>
</form>
<div className="aa-Panel" {...this.autocomplete?.getPanelProps({})}>
{autocompleteState.isOpen &&
autocompleteState.collections.map((collection, index) => {
const { source, items } = collection;
return (
<div key={`source-${index}`} className="aa-Source">
{items.length > 0 && (
<ul
className="aa-List"
{...this.autocomplete?.getListProps()}
>
{items.map((item) => (
<li
key={item.objectID}
className="aa-Item"
{...this.autocomplete?.getItemProps({
item,
source
})}
>
{item.name}
</li>
))}
</ul>
)}
</div>
);
})}
</div>
</div>
);
}
}
export default AutocompleteClass;
Anyway the componentDidMount is called only once, and because of ref object is undefined it just returned from it.
Also messing with this in class components is quite a bad idea (that is why func components are recommended)
My code has 2 classes which are "Forum" and PostEditor". When the button "Post" is clicked which is in the "PostEditor" class, the textarea which has the state "newPostBody" is submitted but the input which has the state "newTitle" is not submitted in ReactJs. Below there is an image of it. What am I doing wrong?
const Post = props => (
<div>
<div >{props.postBody}</div>
</div>
);
class PostEditor extends Component {
constructor(props) {
super(props);
this.state = {
newPostBody: '',
newTitle: ''
};
}
handleInputChange(ev) {
this.setState({
newPostBody: ev.target.value
});
}
handleTitleChange(ev) {
this.setState({
newTitle: ev.target.value
});
}
createPost() {
this.props.addPost(this.state.newPostBody, this.state.newTitle);
this.setState({
newPostBody: '',
newTitle: ''
});
}
render() {
return (
<div>
<div>
<input type="text" value={this.state.newTitle}
onChange={this.handleTitleChange.bind(this)}/>
<textarea value={this.state.newPostBody}
onChange={this.handleInputChange.bind(this)} />
<button onClick={this.createPost.bind(this)}
disabled={!this.state.newPostBody} > Post </button>
</div>
</div>
);
}
}
class Forum extends Component {
constructor(props) {
super(props);
this.state = {
posts: [],
};
}
addPost(newPostBody) {
const newState = Object.assign({}, this.state);
newState.posts.push(newPostBody);
this.setState(newState);
}
render() {
return (
<div>
{this.state.posts.map((postBody, idx) => {
return (
<div >
<Post key={idx} postBody={postBody} />
</div>);
})}
<PostEditor addPost={this.addPost.bind(this)} />
</div>
);
}
}
Your addPost method in Forum class is only expecting body and not expecting 2nd argument which I guess would be title:
You should update it with something like below:
class Forum extends Component {
constructor(props) {
super(props);
this.state = {
posts: [],
};
}
addPost(newPostBody, newPostTitle) {
const newState = Object.assign({}, this.state);
let post = {body: newPostBody, title: newPostTitle}
newState.posts.push(post);
this.setState(newState);
}
render() {
return (
<div>
{this.state.posts.map((post, idx) => {
return (
<div >
<Post key={idx} postBody={post.body} postTitle={post.title} />
</div>);
})}
<PostEditor addPost={this.addPost.bind(this)} />
</div>
);
}
}
Post.js
const Post = props => (
<div>
<div> {props.postTitle} </div>
<div> {props.postBody} </div>
</div>
);
it looks like in your addPost function after clicking the Post button, you are not receiving the same params as the arguments you provided from the createPost:
createPost() {
this.props.addPost(this.state.newPostBody, this.state.newTitle); <== 2 args
this.setState({
newPostBody: '',
newTitle: ''
});
}
vs
addPost(newPostBody) { <== 1 arg only
const newState = Object.assign({}, this.state);
newState.posts.push(newPostBody);
this.setState(newState);
}
I think you want to make the call to addPost look like: this.props.addPost({body: this.state.newPostBody, title: this.state.newTitle})
Then you will have to update the Post UI to handle both title and body values.
I am learning react, and I am making a simple ToDoApp. I set some todo data from a JSON file in the state of my App Component and use the values to populate a Child component. I wrote a method to be called each time the onChange event is fired on a checkbox element and flip the checkbox by updating the state. Thing is this code worked perfectly fine before, but it's not anymore. The state gets updated accordingly when I change the checkbox, but it doesn't update in the child element, I'd like to know why. Here's my code
App.js
import React from "react";
import TodoItem from "./TodoItem";
import toDoData from "./toDosData";
class App extends React.Component {
constructor() {
super();
this.state = {
toDoData: toDoData
};
this.handleOnChange = this.handleOnChange.bind(this);
}
handleOnChange(key)
{
this.setState(prevState => {
let newState = prevState.toDoData.map(currentData => {
if(currentData.id === key)
currentData.completed = !currentData.completed;
return currentData;
});
return {toDoData: newState};
});
}
render() {
let toDoComponents = this.state.toDoData.map(toDoDatum =>
<TodoItem key={toDoDatum.id} details={{
key: toDoDatum.id,
text: toDoDatum.text,
completed: toDoDatum.completed,
onChange: this.handleOnChange
}} />);
return (
<div>
{toDoComponents}
</div>
);
}
}
export default App;
TodoItem.js
import React from "react";
class TodoItem extends React.Component {
properties = this.props.details;
render() {
return (
<div>
<input type="checkbox" checked={this.properties.completed}
onChange={() => this.properties.onChange(this.properties.key)}
/>
<span>{this.properties.text}</span>
</div>
)
}
}
export default TodoItem;
Thanks in advance.
Why do you need to assign your details prop to properties in your class? If you do that properties does not reflect the prop changes and your child component can't see the updates. Just use the props as it is:
render() {
const { details } = this.props;
return (
<div>
<input
type="checkbox"
checked={details.completed}
onChange={() => details.onChange(details.key)}
/>
<span>{details.text}</span>
</div>
);
}
}
Also, since you don't use any state or lifecycle method in TodoItem component, it can be a functional component as well.
const TodoItem = ({ details }) => (
<div>
<input
type="checkbox"
checked={details.completed}
onChange={() => details.onChange(details.key)}
/>
<span>{details.text}</span>
</div>
);
One more thing, why don't you pass the todo itself to TodoItem directly?
<TodoItem
key={toDoDatum.id}
todo={toDoDatum}
onChange={this.handleOnChange}
/>
and
const TodoItem = ({ todo, onChange }) => (
<div>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onChange(todo.id)}
/>
<span>{todo.text}</span>
</div>
);
Isn't this more readable?
Update after comment
const toDoData = [
{ id: 1, text: "foo", completed: false },
{ id: 2, text: "bar", completed: false },
{ id: 3, text: "baz", completed: false }
];
const TodoItem = ({ todo, onChange }) => (
<div>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onChange(todo.id)}
/>
<span>{todo.text}</span>
</div>
);
class App extends React.Component {
constructor() {
super();
this.state = {
toDoData: toDoData
};
this.handleOnChange = this.handleOnChange.bind(this);
}
handleOnChange(key) {
this.setState(prevState => {
let newState = prevState.toDoData.map(currentData => {
if (currentData.id === key)
currentData.completed = !currentData.completed;
return currentData;
});
return { toDoData: newState };
});
}
render() {
let toDoComponents = this.state.toDoData.map(toDoDatum => (
<TodoItem
key={toDoDatum.id}
todo={toDoDatum}
onChange={this.handleOnChange}
/>
));
return <div>{toDoComponents}</div>;
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root" />
I'm working on my First project with React, I have an App and a ToDo. I am defining a deleteToDo method and I want the method to call this.setState() and pass it a new array that doesn't have the to-do item being deleted with the use of the .filter() array method. I don't want to alter the code to much or introduce more complexity. In essence I would like to keep it as straight forward as possible. I am still a beginner with React so this has been a big learning process. I feel that I am close.
This is the main app
import React, { Component } from 'react';
import './App.css';
import ToDo from './components/ToDo.js';
class App extends Component {
constructor(props) {
super(props);
this.state = {
todos: [
{ description: 'Walk the cat', isCompleted: true },
{ description: 'Throw the dishes away', isCompleted: false },
{ description: 'Buy new dishes', isCompleted: false }
],
newTodoDescription: ''
};
}
deleteToDo(index) {
const todos = this.state.todos.slice();
const todo = todos[index];
todo.deleteToDo = this.state.filter(index);
this.setState({ todos: todos });
}
handleChange(e) {
this.setState({ newTodoDescription: e.target.value })
}
handleSubmit(e) {
e.preventDefault();
if (!this.state.newTodoDescription) { return }
const newTodo = { description: this.state.newTodoDescription, isCompleted: false };
this.setState({ todos: [...this.state.todos, newTodo], newTodoDescription: '' });
}
toggleComplete(index) {
const todos = this.state.todos.slice();
const todo = todos[index];
todo.isCompleted = todo.isCompleted ? false : true;
this.setState({ todos: todos });
}
render() {
return (
<div className="App">
<ul>
{ this.state.todos.map( (todo, index) =>
<ToDo key={ index } description={ todo.description } isCompleted={ todo.isCompleted } toggleComplete={ this.toggleComplete } deleteToDo={ this.deleteToDo } />
)}
</ul>
<form onSubmit={ (e) => this.handleSubmit(e) }>
<input type="text" value={ this.state.newTodoDescription } onChange={ (e) => this.handleChange(e) } />
<input type="submit" />
</form>
</div>
);
}
}
export default App;
And this the ToDo aspect
import React, { Component } from 'react';
class ToDo extends Component {
render() {
return (
<li>
<button type="button" onClick={ this.props.deleteTodo} > delete </button>
<input type="checkbox" checked={ this.props.isCompleted } onChange={ this.props.toggleComplete } />
<span>{ this.props.description }</span>
</li>
);
}
}
export default ToDo;
You slice and array without the index, that's may be why your delete not work
deleteToDo(index) {
const todos = this.state.todos.slice(index, 1);
this.setState({ todos: todos });
}
1) You need to bind your deleteToDo method in the constructor
this.deleteToDo = this.deleteToDo.bind(this);
2) You need to set a new property on the component that is the same as its index.
<ToDo
key={index}
id={index}
description={ todo.description }
// ...
/>
3) Then you can pass that index as the argument to deleteToDo (making sure you spell the method name correctly).
<button
type="button"
onClick={() => this.props.deleteToDo(this.props.index)}
>Delete
</button>
4) Finally, you can strip down your deleteToDo method to the following:
deleteToDo(index) {
// Return a new array that doesn't
// have a row with a matching index
const todos = this.state.todos.filter((el, i) => i !== index);
this.setState({ todos });
}
Here's a working version.
I have two components named parent and child.
Child component contains checkbox's and it will send selected checkbox's values to Parent component
Child
class Child extends Component{
state = {
options: [],
selected_options: []
}
handleClose = (e) => {
this.props.add(this.state.selected_options)
}
handleChange = (event) => {
console.log(event.target.name);
if(event.target.checked==true){
event.target.checked=false
this.state.selected_options.slice(this.state.options.indexOf(event.target.value),1)
}
else{
event.target.checked = true
this.state.selected_options.push(event.target.value)
}
}
render() {
return(
<div>
<Grid>
{
this.state.options.map(value => {
return(
<Checkbox onChange={this.handleChange} label={value.name} value={value.name} checked={false} />
)
})
}
<Button color='green' onClick={this.handleClose} inverted>
<Icon name='checkmark' /> Apply
</Button>
</div>
);
}
}
and Parent Component
class Parent extends Component {
constructor(props){
super(props);
this.state = {
selected_options:[],
}
}
addOptions = (options) => {
this.setState({
selected_options: options
})
}
render() {
return(
<div>
<Child selected_options={this.state.selected_options} add={this.addOptions} />
</div>
);
}
}
When a checkbox is selected it must output its corresponding value in the console. but it showing undefined
Directly mutating the state or the event value is not the correct idea
You should be doing it like
class Child extends Component{
state = {
checkedState: []
options: [],
selected_options: []
}
handleClose = (e) => {
this.props.add(this.state.selected_options)
}
handleChange = (index, value) => {
var checkedState = [...this.state.checkedState];
if(checkedState[index] === undefined) {
checkedState.push(true)
}
else {
if(checkedState[index] === true) {
var selected_options=[...this.state.selected_options];
var idx = selected_options.findIndex((obj) => obj.name == value)
selected_options.splice(idx, 1);
checkedState[index] = false
} else {
var selected_options=[...this.state.selected_options];
selected_options.push(value);
}
}
this.setState({checkedState, selected_options})
}
render() {
return(
<div>
<Grid>
{
this.state.options.map((value, index) => {
return(
<Checkbox onChange={() => this.handleChange(index, value.name)} label={value.name} value={value.name} checked={this.state.checkedState[index] || false} />
)
})
}
<Button color='green' onClick={this.handleClose} inverted>
<Icon name='checkmark' /> Apply
</Button>
</div>
);
}
}
and from the Parent render the child and not the Parent
Parent
render() {
return(
<div>
<Child selected_options={this.state.selected_options} add={this.addOptions} />
</div>
);
}