I'm currently teaching my self Redux. For this purpose I created a simple Todo application. Now on this application I'm currently at the process of using dispatch() to put a todo into my store. This is a question about your opinion. I want to avoid code smell.
I found two ways of achieving this. One using state and one using ref. I was wondering which way is better? Thank you for any advice. The two versions are below.
Version one using ref:
import React, { Component } from "react";
import Todo from "./Todo";
import { connect } from "react-redux";
import { ADD_TODO } from "./actionCreators";
class TodoList extends Component {
taskRef = React.createRef();
handleSubmit = event => {
event.preventDefault();
this.props.dispatch({
type: ADD_TODO,
task: this.taskRef.current.value
});
event.currentTarget.reset();
};
render() {
let todos = this.props.todos.map((val, index) => (
<Todo task={val.task} key={index} />
));
return (
<div>
<form onSubmit={this.handleSubmit}>
<label htmlFor="task">Task </label>
<input type="text" name="task" id="task" ref={this.taskRef} />
<button type="submit">Add a Todo!</button>
</form>
<ul>{todos}</ul>
</div>
);
}
}
const mapDispatchToProps = state => ({
todos: state.todos
});
export default connect(mapDispatchToProps)(TodoList);
And here is the second version using state:
import React, { Component } from "react";
import Todo from "./Todo";
import { connect } from "react-redux";
import { ADD_TODO } from "./actionCreators";
class TodoList extends Component {
state = {
task: ""
};
handleSubmit = event => {
event.preventDefault();
this.props.dispatch({
type: ADD_TODO,
task: this.state.task
});
event.target.reset();
};
handleChange = event => {
event.persist();
this.setState((state, props) => ({
[event.target.name]: event.target.value
}));
};
render() {
let todos = this.props.todos.map((val, index) => (
<Todo task={val.task} key={index} />
));
return (
<div>
<form onSubmit={this.handleSubmit}>
<label htmlFor="task">Task </label>
<input
type="text"
name="task"
id="task"
onChange={this.handleChange}
/>
<button type="submit">Add a Todo!</button>
</form>
<ul>{todos}</ul>
</div>
);
}
}
const mapDispatchToProps = state => ({
todos: state.todos
});
export default connect(mapDispatchToProps)(TodoList);
EDIT: As qasimalbaqali pointed out in the comments here is a similar post on stackoverflow. I'm still unsure, because the first answer says refs are bad with reasons, the second one says React Devs say refs are awesome for grabbing values from the dom (which is what I'm doing!).
Thank you for anyone helping. It seems like a majority of the community is in favor of using state.
I also asked Dan Abramov, who said that he'd prefer a ref in this case.
You can see his answer here.
Thank you everyone for your input and advice! :)
Related
I am new to React and struggle to transform this function component into a class component with a constructor(). I can't figure out how to transform the functions happening onSubmit and onClick.
Thank you very much.
The function component:
import React, { useState, Component } from 'react';
import { render } from 'react-dom';
import './Options.css';
const Test = (props) => {
const [links, setLinks] = useState([]);
const [link, setLink] = useState('');
function handleSubmit(e) {
e.preventDefault();
const newLink = {
id: new Date().getTime(),
text: link,
};
setLinks([...links].concat(newLink));
setLink('');
}
function deleteLink(id) {
const updatedLinks = [...links].filter((link) => link.id !== id);
setLinks(updatedLinks);
}
return (
<div className="OptionsContainer">
<form onSubmit={handleSubmit}>
<input
type="text"
onChange={(e) => setLink(e.target.value)}
value={link}
/>
<button type="submit"> + </button>
</form>
{links.map((link) => (
<div key={link.id}>
<div>{link.text}</div>
<button onClick={() => deleteLink(link.id)}>Remove</button>
</div>
))}
</div>
);
};
export default Test;
When we work with class components a couple of things got changed, we have a new object this.state, a new function this.setState, you have to bind your functions on the constructor if you want to have access to this inside it. I think you'll learn way more if you read the code, so here this is your component as a class component:
import React, { Component } from 'react';
import './Options.css';
class App extends Component {
constructor() {
super()
this.state = {
links: [],
link: '',
}
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(e) {
e.preventDefault();
const newLink = {
id: new Date().getTime(),
text: this.state.link,
};
this.setState({links: [...this.state.links, newLink], link: ''});
}
deleteLink(id) {
const updatedLinks = [...this.state.links].filter((link) => link.id !== id);
this.setState({...this.state, links: updatedLinks });
}
render() {
console.log(this.state)
return (
<div className="OptionsContainer">
<form onSubmit={this.handleSubmit}>
<input
type="text"
onChange={(e) => this.setState({ ...this.state, link: e.target.value})}
value={this.state.link}
/>
<button type="submit"> + </button>
</form>
{this.state.links.map((link, index) => (
<div key={link.id}>
<span>{link.text}</span>
<button onClick={() => this.deleteLink(link.id)}>Remove</button>
</div>
))}
</div>
);
}
};
export default Test;
Without seeing your original functional component, it's going to be hard to tell you what the class component should look like.
To answer your question about why onSubmit might not be working, it's because onSubmit is expecting to be passed an uncalled function, so that the <form> element can call that function itself when the time is right.
You are currently calling the function this.handleOnSubmit(), which returns nothing, so no handler for the form submit is properly assigned. Try removing the call to the function, like this:
<form onSubmit={this.handleSubmit}>
I am trying to submit a form data to the reducer via action creators but its not working anyway.The page is reloading anyway.I can't resist.
Below is my code
import React, { Component } from 'react';
import { connect } from 'react-redux'
import {addName} from '../Actions/actionCreators'
class about extends Component {
constructor(props){
super(props)
}
render() {
return (
<div>
<h2> About me </h2>
<form action="" onSubmit={this.submitform}>
<br/>
<input type="text" name="name" placeholder="Your Name" />
<br/>
<input type="submit" value="Submit"/>
</form>
</div>
);
}
}
const mapDispatchToProps = dispatch => {
return{
submitform : (e) => {
dispatch(addName(e.target.name.value))
}
}
}
export default connect(null, mapDispatchToProps)(about)
Here addName is the action creator which receives data(name)
if i am using this,also error happens 'this.props.dispatch` is not a function
constructor(props){
super(props)
this.submithandle = this.submithandle.bind(this)
}
submithandle(e, dispatch){
e.preventDefault()
this.props.dispatch(addName(e.target.name.value))
}
What step i can take to submit data via mapDispatchToProps?
You don't understand mapDispatchToProps correctly. mapDispatchToProps will add the functions to the delivered props of the component. The props of about looks like this:
{submitform: (e) => {dispatch(addName(e.target.name.value))}}
To call the submitform prop, you have to access it with this.props.submitform.
If you write that into your onSubmit prop of form, it should work.
You should also set the names of your react components to start with an upper-case so that react can differentiate them between native and new components. So about => About.
Hope this helps. Happy coding.
You need a handler for the onSubmit event.
this.handleSubmit(e)=>{
this.props.addNameAction(e.target.value);
}
then Dispatch the action that you supposedly imported at the top.
const mapDispatchToProps = dispatch => {
return{
addNameAction : (name) => {
dispatch(addName(name))
}
}
}
I intend to create a to-do list without using ref as in the many examples, but it isn't working.
The expected behavior is that upon entering an entry, it will show up at the top and upon clicking add, it will create an input box for entering an entry. Currently, upon entering the state returns undefined.
The code can be found below or in this sandbox:
import React, { Component } from 'react';
import { render } from 'react-dom';
import './style.css';
import ToDo from './todo'
class App extends Component {
constructor() {
super();
this.state = {
todos: []
};
}
onChange=(e)=>{
const newToDos = [...this.state.todos]
newToDos.push(e.target.value)
this.setState({
todos: newToDos
})
}
onAdd=(e)=>{
e.preventDefault();
const newtodos=[...this.state.todos]
this.setState({
todos: newtodos.push("")
})
}
render() {
console.log(this.state.todos)
return (
<div>
<p>All the to-dos include {this.state.todos}</p>
<ToDo
todos={this.state.todos}
/>
<form onSubmit={this.onChange}>
<input
type="text"
placeholder="add a new todo..."
/>
</form>
<button onClick={this.onAdd}>+</button>
</div>
);
}
}
render(<App />, document.getElementById('root'));
And here is the todo.js:
import React, { Component } from 'react';
import { render } from 'react-dom';
export default class ToDo extends Component {
constructor(props){
super(props)
}
render() {
const {todos, onChange}=this.props
return (
<div>
{
todos.map((todo, index)=>
<div>
{todo}
</div>
)
}
</div>
);
}
}
You can store your new todo in state when onChange input and add this into todos when click save.
I have forked and edit your sample.
https://stackblitz.com/edit/react-nwtp5g?file=index.js
BTW: In your sample, newtodos.push("") will return the length of newtodos array, not the array after pushed.
onAdd=(e)=>{
e.preventDefault();
const newtodos=[...this.state.todos]
this.setState({
todos: newtodos.push("")
})
Hope this help.
your code newtodos.push("") dosent return array so no map function:
this.setState({
todos: newtodos.push("")
})
correct it something like this
this.setState({
todos: newtodos.concat("new value")
})
You have a problem with this code,
<form onSubmit={this.onChange}>
<input
type="text"
placeholder="add a new todo..."
/>
</form>
Here you are adding onSubmit on form, which will never call because you don't have submit button.
you should do something like this,
<form>
<input
type="text"
placeholder="add a new todo..."
onChange={this.onChange}
value={this.state.currentValue}
/>
</form>
onChange=(e)=>{
event.preventDefault();
this.setState({
currentValue: e.target.value
})
}
onAdd=(e)=>{
e.preventDefault();
const newToDos = [...this.state.todos]
newToDos.push(this.state.currentValue)
this.setState({
todos: newToDos,
currentValue: ''
})
}
Demo
Update
In your todo component you have useless constructor, If you don't have state in a component or don't have any function to bind this don't add constructor.
You can remove the constructor.
Another thing is, you are not passing any onChange prop to todo component, so here you will get undefined for onChange.
const {todos, onChange}=this.props
You can also write this component as a functional component.
You can update your code with
import React, { Component } from 'react';
import { render } from 'react-dom';
import './style.css';
import ToDo from './todo'
class App extends Component {
constructor() {
super();
this.state = {
todos: [],
inputText: ""
};
}
onAdd= () => {
this.setState({
todos: [...this.state.todos, this.state.inputText], textInput: ""
})
}
render() {
console.log(this.state.todos)
return (
<div>
<p>All the to-dos include {this.state.todos}</p>
<ToDo
todos={this.state.todos}
/>
<form>
<input
type="text"
placeholder="add a new todo..."
onChange={inputText => this.setState({inputText})}
/>
</form>
<button onClick={this.onAdd}>+</button>
</div>
);
}
}
render(<App />, document.getElementById('root'));
And in todo.js you can simply do
import React, { Component } from 'react';
import { render } from 'react-dom';
export default const ToDo = ({todos}) => {
return(<div>
{todos.map((todo, index) => (
<div key={index}>
{todo}
</div>))}
</div>)}
as it do not contains any state associated with it.
I am adding a feature to the survey form review for a user to be able to upload files and my concern is that I do not want to mutate state with this implementation, how do I refactor the below to ensure this? Is my only option refactoring it to a class-based component?
// SurveyFormReview shows users their form inputs for review
import _ from "lodash";
import React from "react";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import formFields from "./formFields";
import * as actions from "../../actions";
export const onFileChange = event => {
this.setState({ file: event.target.files });
};
const SurveyFormReview = ({ onCancel, formValues, submitSurvey, history }) => {
this.state = { file: null };
const reviewFields = _.map(formFields, ({ name, label }) => {
return (
<div key={name}>
<label>{label}</label>
<label>{formValues[name]}</label>
</div>
);
});
return (
<div>
<h5>Please confirm your entries</h5>
{reviewFields}
<h5>Add an Image</h5>
<input
onChange={this.onFileChange.bind(this)}
type="file"
accept="image/*"
/>
Or do I have no choice except to refactor this to a class-based component as a best course?
You should refactor this into a class. Something on the lines of this should work
class SurveyFormReview extends React.Component {
state = { file: null };
onFileChange = event => {
this.setState({ file: event.target.files });
};
render() {
const { onCancel, formValues, submitSurvey, history } = this.props
const reviewFields = _.map(formFields, ({ name, label }) => {
return (
<div key={name}>
<label>{label}</label>
<label>{formValues[name]}</label>
</div>
);
});
return (
<div>
<h5>Please confirm your entries</h5>
{reviewFields}
<h5>Add an Image</h5>
<input
onChange={this.onFileChange}
type="file"
accept="image/*"
/>
</div>
)
}
}
just as a note about optimizations and stuff.
Because this is a form, I'd recommend you use better html elements.
const reviewFields = _.map(formFields, ({ name, label }) => {
return (
<fieldset key={name}>
<span>{label}</span>
<span>{formValues[name]}</span>
</fieldset>
);
});
label elements are usually used with input elements.
fieldset elements are usually for form groups of data
you could use a legend element for the title of your fieldset if you wanted :)
If you don't want to touch existing code, you can create HOC
const withFile = (Component) => class extends React.Component {
state = { file: null }
render() {
return <Component {...this.props} file={file} onAttach={files => this.setState({ file: files }) />
}
}
export default withFile(SurveyForm)
Now your form will receive file and onAttach as props.
I have a component with an input that when submitted is meant to pass the input text to store. I can't figure out how to preventDefault() when I submit the form.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { addItem } from '../actions';
const ItemInput = (props) => {
return (
<div>
<form onSubmit={() => props.addItem('test')}>
<input
placeholder="addItem"
/>
</form>
</div>
);
}
const mapStateToProps = (state) => {
return { addItem: state.addItem };
}
export default connect(mapStateToProps, {
addItem: addItem
})(ItemInput);
I know how to do this in react, but it doesn't seem to work the same way, I keep getting an unexpected token error that doesn't make any sense, probably because the syntax just doesn't work the same way with redux and store. This also isn't a button issue, I'm submitting the form after pressing return.
This part of your code is just a function, you can expand it as you want:
<form onSubmit={() => props.addItem('test')}>
So, you can do:
<form onSubmit={e => {
e.preventDefault();
props.addItem('test');
}}>
Or move this handler into a function:
const handleSubmit = e => {
e.preventDefault();
props.addItem('test');
}
// ...
<form onSubmit={handleSubmit}>