there are similiar questions in stackoverflow but I I did not find what I was looking for.
I have a donorDonationForm which is a class componenet that connected to the redux state. The porpuse of that componenet is to collect inormation about a person that want to donate electronics items. At this point, I want to save those items in an array (maybe with an object in the future).
my redux state save the donor info and the reducer looks like this:
import {CHANGE_INPUT_FIELD} from '../utils/constants';
const initialStateInputs = {
// update the state
donorFields: {
name: '',
phone: '',
area: '',
yeshuv: '',
address: ''
// dateOfOffer: ''
},
donationFields: {
// donorID: '',
// vulonteerID: '',
type: [],
quantity: 1,
status: 'NOT_HANDLED',
comments: ''
// lastDateHandled: ''
}
// }, items: [ //need to add quantity
// {id: 1, name: "LAPTOP", isChecked: false, label: 'מחשב'},
// {id: 2, name: "HEADPHONES", isChecked: false, label: 'אוזניות'},
// {id: 3, name: "OTHER", isChecked: false, label: 'אחר'},
// ]
}
export const donorDonationInputsReducer = ( state = initialStateInputs, action={} ) => {
switch(action.type) {
case CHANGE_INPUT_FIELD:
return Object.assign( {}, state,
{
donorFields : {...state.donorFields,...action.payload},
donationFields: {...state.donationFields,...action.payload},
// items : {...state.items,...action.payload},
// isChecked: action.payload
})
default:
return state;
}
}
As you can see the items is commented by now, and I am managing the state of the item in a local state, and that how the comp looks like:
import React, {Component} from 'react';
import { connect } from 'react-redux';
import { setInputField } from '../actions/formAction';
import CheckBox from '../components/CheckBox/CheckBox';
import FormInput from '../components/FormInput/FormInput';
import {selectAreasOptions_2} from '../utils/constants';
import "./form.css";
const mapStateToProps = (state) => {
return {
donorFields: state.donorDonationInputsReducer.donorFields,
donationFields: state.donorDonationInputsReducer.donationFields
}
}
const mapDispatchToProps = dispatch => {
return {
onInputChange: event => {
const {name, value} = event.target;
dispatch(setInputField( { [name]:value} ) )
}
}
}
class donorDonationForm extends Component {
constructor() {
super();
this.state = {
items: [
{id: 1, name: "LAPTOP", isChecked: false, label: 'מחשב'},
{id: 2, name: "HEADPHONES", isChecked: false, label: 'אוזניות'},
{id: 3, name: "OTHER", isChecked: false, label: 'אחר'},
]
,
type: []
}
}
handleCheckChieldElement = (event) => {
let {items, type} = this.state;
let arr = [];
items.forEach(item => {
if (item.name === event.target.value) {
item.isChecked = event.target.checked;
// console.log(`item.name :${item.name }`);
// console.log(`event.target.value :${event.target.value}`);
// console.log(`event.target.checked :${event.target.checked}`);
}
})
items.map(item => item.isChecked ? arr.push(item.name) : null)
this.setState({items: [...items], type: [...arr]});
}
onButtonSubmit = (event) => {
console.log(this.props.donorFields);
event.preventDefault();
fetch('http://localhost:8000/api/donor', {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
...this.props.donorFields
})
})
.then(response => response.json())
.then(resp => console.log(resp))
.catch( err => console.log(err) )
}
// componentDidUpdate(prevProps, prevState) {
// const {items, type} = this.state;
// // const type = [];
// if (prevState.items !== items) {
// console.log('items state has changed');
// items.map (item => item.isChecked ?
// this.setState({type: [...type,item.name]}) : null)
// // if (item.isChecked) { type.push(item.name) } ;
// console.log(type);
// }
// }
render() {
console.log(this.state.items);
console.log(this.state.type);
const { onInputChange } = this.props;
return (
<div>
<h1 className="pt4"> פרטי תורם</h1>
<form className=" black-80 pt2" >
<section className=" grid-container">
<FormInput
id="name"
name="name"
type="text"
onInputChange={onInputChange}
label="שם "
required
/>
<FormInput
id="phone"
name="phone"
type="tel"
onInputChange={onInputChange}
label="מספר טלפון "
required
/>
<FormInput
id="address"
name="address"
type="text"
onInputChange={onInputChange}
label="כתובת "
required
/>
<FormInput
id="yeshuv"
name="yeshuv"
type="text"
onInputChange={onInputChange}
label="עיר "
required
/>
<FormInput
id="comments"
name="comments"
onInputChange={onInputChange}
label="הערות "
required
/>
<FormInput
id="area"
name="area"
onInputChange={onInputChange}
label="איזור "
select={selectAreasOptions_2}
/>
{/* type */}
<div className="measure-narrow">
<label htmlFor="type" className="f5 b db mb2">מעוניין לתרום
<span className="normal black-60"> *</span>
</label>
{
this.state.items.map( (item, i) => {
return (
<CheckBox
key={i}
onChange={this.handleCheckChieldElement}
checked={ item.isChecked }
value= {item.name}
label = {item.label}
/>
);
})
}
</div>
</section>
<input type="submit" value="שלח"
className="b bg-light-blue pa2 hover pointer"
onClick={this.onButtonSubmit}
/>
</form>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(donorDonationForm);
My main goal is that the type array - the final donation, will update the redux state before submitting this form. I tried with componentDidUpdate but didn't make it. What is the best way for tracking the checked items, updating the array and then update the type array which is the final donation in the redux state? should I do that in the onButtonSubmit method - before sending the data to the server (and thats way saving the looping over the items array for searching the checked elements) ?
Better approach would be do inside onButtonSubmit
Let me briefly explain the tasks:
inputChangeHandler to update this.state.items
Go with the final this.state.items value Array of items inside onButtonSubmit
After getting API response update the application level Redux state with Array of items.
Note: Dispatch the action. Reducer will update the Redux state. Following code will do this:
// Action
export const setItems = (data) => (dispatch) => {
dispatch({type: 'SET_ITEMS', payload: data})
}
// mapDispatchToProps
const mapDispatchToProps = (dispatch) =>
bindActionCreators(
{
setItems,
...others
},
dispatch
)
// onSubmitButton
onButtonSubmit = (event) => {
console.log(this.props.donorFields);
event.preventDefault();
fetch('http://localhost:8000/api/donor', {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
...this.props.donorFields
})
})
.then(response => this.props.setItems(response.json())) // will update the state.
.then(resp => console.log(resp))
.catch( err => console.log(err) )
}
// Reducer
export const donorDonationInputsReducer = ( state = initialStateInputs, action={} ) => {
switch(action.type) {
case CHANGE_INPUT_FIELD:
return Object.assign( {}, state,
{
donorFields : {...state.donorFields,...action.payload},
donationFields: {...state.donationFields,...action.payload},
// items : {...state.items,...action.payload},
// isChecked: action.payload
})
case SET_ITEMS:
return {
...state,
items: action.payload
}
default:
return state;
}
}
That's it.
Happy Coding :)
Related
state.firebase.profile always is undefined when I reload by browser.
Somehow, it goes well except for F5 as far as I can see.
I check by using console.log("TEST HERE"+ JSON.stringify(this.props.profile.name));.
Where should I modify it...
class ReagtTagSample extends Component {
constructor(props) {
super(props);
this.state = {
porco:""
tags: [{ id: 'Yugoslavia', text: 'Yugoslavia' }, { id: 'India', text: 'India' }],
suggestions: [
{ id: "England", text: "England" },
{ id: "Mexico", text: "Mexico" },
],
};
componentDidMount=()=>{
console.log("TEST HERE"+ JSON.stringify(this.props.profile.name));
}
handleAddition(tag) {
this.setState((state) => ({ tags: [...state.tags, tag] }));
}
handleDrag(tag, currPos, newPos) {
const tags = [...this.state.tags];
const newTags = tags.slice();
newTags.splice(currPos, 1);
newTags.splice(newPos, 0, tag);
this.setState({ tags: newTags });
}
//ommit
render() {
const { auth, authError, profile } = this.props;
return (
//ommit
const mapStateToProps = (state) => {
return {
auth: state.firebase.auth,
authError: state.auth.authError,
profile: state.firebase.profile,
};
};
const mapDispatchToProps = (dispatch) => {
return {
profileUpdate: (user) => dispatch(Update(user)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Update);
Update= (user) => {
return (dispatch, getState, { getFirebase, getFirestore }) => {
const firestore = getFirestore();
const firebase = getFirebase();
const profile = getState().firebase.profile;
const authorId = getState().firebase.auth.uid;
firestore.collection('users').doc(authorId).set({
name: user.userName,
tags:user.tags,
}).then(() => {
dispatch({ type: 'PROFILE_UPDATE_SUCCESS' })
}).catch(err => {
dispatch({ type: 'PROFILE_UPDATE_ERROR', err })
})
}
}
I would like to use profile.name as default input name...
<div className="input-field">
<label htmlFor="userName">DisplayName</label>
<input
type="text"
id="userName"
value={this.state.userName}
onChange={this.handleChange}
/>
React state and props will be reset to their initial values when we reload the web app in browser using F5 or refresh button (because the app restarts as fresh).
The console log in componentDidMount prints undefined:
componentDidMount = () => {
console.log("TEST HERE" + JSON.stringify(this.props.profile.name));
// side node: you do not really need an arrow function here as it is a special
// lifecycle method. It will the `this` automatically binded with component instance
}
because, probably you are getting this.props.profile data through an API call. Hence, this.props.profile will receive its values asynchronously. You can see it on console log in componentDidUpdate lifecycle method.
Solution:
But if you want to set the default value of below input from this.props.profile.name, you can use either of these options:
Option 1: Using key and defaultValue. It will work because React components or elements re-render when their key is changed. And due to re-render it will read new defaultValue.
<input
key={this.props.profile.name}
defaultValue={this.props.profile.name}
type="text"
id="userName"
value={this.state.userName}
onChange={this.handleChange}
/>
Option 2: Set the userName in state when data is available in props:
componentDidUpdate(prevProps, prevState) {
if (this.props.profile.name !== prevProps.profile.name) {
this.setState({
userName: this.props.profile.name,
})
}
}
...
<input
type="text"
id="userName"
value={this.state.userName}
onChange={this.handleChange}
/>
When i call this function
getQuestions = () => {
this.setState({ loading: true })
const { data } = this.props
axios
.get(data.questions)
.then((res) => {
this.setState({
loading: false,
questions: res.data,
})
this.initialQuestions = res.data
})
.catch((err) =>
this.setState({
loading: false,
questions: [],
})
)
}
it updates the array questions in state and array initialQuestions variable in constructor. The state questions represents the values form inputs. The inputs are handled in child component with this code
onChange = (e) => {
const { hasChanged, setQuestions } = this.props
// Update questions
let questions = this.props.questions
questions[e.target.getAttribute('data-id')][e.target.name] =
e.target.value
setQuestions(questions)
}
setQuestions is passed in props as setQuestions={(state) => this.setState({ questions: state })}
So when i change the value of inputs the onChange function is called and it changes the parent component questions in state. But the parent variable this.initialQuestions also is being changed to the questions value from state, but I don't know why
Edit:
That's the code you should be able to run
const { Component } = React;
const Textarea = "textarea";
const objectsEquals = (obj1, obj2) =>
Object.keys(obj1).length === Object.keys(obj2).length &&
Object.keys(obj1).every((p) => obj1[p] === obj2[p])
class QuestionList extends React.Component {
static propTypes = {
questions: PropTypes.array,
removeQuestion: PropTypes.func.isRequired,
hasChanged: PropTypes.func.isRequired,
setQuestions: PropTypes.func.isRequired,
}
constructor(props) {
super(props)
this.questions = props.questions
this.onChange = this.onChange.bind(this)
}
onChange = (e) => {
const { hasChanged, setQuestions } = this.props
// Update questions
let questions = this.props.questions
questions[e.target.getAttribute('data-id')][e.target.name] =
e.target.value
setQuestions(questions)
if (hasChanged && this.questions.length > 0) {
// array of booleans, true if object has change otherwise false
const hasChangedArray = this.props.questions.map(
(_, index) =>
!objectsEquals(
this.questions[index],
this.props.questions[index]
)
)
console.log("hasChangedArray = ", hasChangedArray)
console.log("this.questions[0] = ", this.questions[0])
console.log("this.props.questions[0] = ", this.props.questions[0])
// If true in array than the form has changed
hasChanged(
hasChangedArray.some((hasChanged) => hasChanged === true)
)
}
}
render() {
const { removeQuestion, questions } = this.props
const questionList = questions.map((question, index) => (
<div className="card" key={index}>
<div className="card__body">
<div className="row">
<div className="col-sm-7">
<div className="form-control">
<label className="form-control__label">
Question:
</label>
<input
type="text"
id={`question-${index}`}
data-id={index}
onChange={this.onChange}
name="question"
value={
this.props.questions[index].question
}
className="form-control__input form control__textarea"
placeholder="Pass the question..."
rows="3"
/>
</div>
<div className="form-control">
<label className="form-control__label">
Summery:
</label>
<Textarea
id={`summery-${index}`}
data-id={index}
onChange={this.onChange}
name="summery"
value={this.props.questions[index].summery}
className="form-control__input form-control__textarea"
placeholder="Pass the summery..."
rows="3"
/>
</div>
</div>
</div>
</div>
</div>
))
return questionList
}
}
class Questions extends React.Component {
constructor(props) {
super(props)
this.initialQuestions = []
this.state = {
loading: true,
questions: [],
hasChanged: false,
}
this.getQuestions = this.getQuestions.bind(this)
this.resetForm = this.resetForm.bind(this)
}
resetForm = () => {
console.log("this.initialQuestions =", this.initialQuestions)
this.setState({
questions: this.initialQuestions,
hasChanged: false,
})
}
getQuestions = () => {
this.setState({ loading: true })
const { data } = this.props
// axios
// .get(data.questions)
// .then((res) => {
// this.setState({
// loading: false,
// questions: res.data,
// })
// this.initialQuestions = res.data
// })
// .catch((err) =>
// this.setState({
// loading: false,
// questions: [],
// })
// )
// You can't do a database request so here is some example code
this.setState({
loading: false,
questions: [
{
question: 'example-question',
summery: 'example-summery',
},
{
question: 'example-question-2',
summery: 'example-summery-2',
},
],
})
this.initialQuestions = [
{
question: 'example-question',
summery: 'example-summery',
},
{
question: 'example-question-2',
summery: 'example-summery-2',
},
]
}
componentDidMount = () => this.getQuestions()
render() {
const { loading, questions, hasChanged } = this.state
if (loading) return <h1>Loading...</h1>
return (
<form>
<QuestionList
questions={questions}
hasChanged={(state) =>
this.setState({ hasChanged: state })
}
setQuestions={(state) =>
this.setState({ questions: state })
}
/>
<button
type="reset"
onClick={this.resetForm}
className={`btn ${
!hasChanged
? 'btn__disabled'
: ''
}`}
>
Cancel
</button>
<button
type="submit"
className={`btn btn__contrast ${
!hasChanged
? 'btn__disabled'
: ''
}`}
>
Save
</button>
</form>
)
}
}
ReactDOM.render(<Questions />, document.querySelector("#root"));
<script src="https://unpkg.com/react#17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/prop-types#15/prop-types.min.js"></script>
<div id="root"></div>
Both state questions and class variable initialQuestions hold reference of res.data. Now when you update questions in onChange method, you are updating it by reference i.e directly mutating it and hence the class variable is also updated
You must not update it by reference but clone and update like below
onChange = (e) => {
const { hasChanged, setQuestions } = this.props
// Update questions
let questions = this.props.questions
questions = questions.map((question, idx) => {
if(idx === e.target.getAttribute('data-id')) {
return {
...question,
[e.target.name]: e.target.value
}
}
return question;
});
setQuestions(questions)
}
I am using MERN and Redux.
I have a clickHandler function that calls a findAuthor function which is imported from my actions. This finds a user by their id and returns it. I have added the user to the global state. I want to then retrieve the user and add their name to local state but i can't get this working. I keep getting this error TypeError: this.props.subAuthor is undefined. What am i missing here? When i try just printing to console i get no object showing until the second click. How do i get it t update straight away?
import React, { Component } from "react";
import PropTypes from "prop-types";
import GoogleSearch from "./GoogleSearch";
import { connect } from "react-redux";
import { fetchSubjects } from "../../actions/subject";
import { fetchComments } from "../../actions/comment";
import { updateSubject } from "../../actions/subject";
import { getUser } from "../../actions/authActions";
class Subject extends Component {
// on loading the subjects and comments
// are fetched from the database
componentDidMount() {
this.props.fetchSubjects();
this.props.fetchComments();
}
constructor(props) {
super(props);
this.state = {
// set inital state for subjects
// description, summary and comments all invisible
viewDesription: -1,
viewSummary: -1,
comments: [],
name: "",
};
}
componentWillReceiveProps(nextProps) {
// new subject and comments are added to the top
// of the arrays
if (nextProps.newPost) {
this.props.subjects.unshift(nextProps.newPost);
}
if (nextProps.newPost) {
this.props.comments.unshift(nextProps.newPost);
}
}
clickHandler = (id) => {
// when a subject title is clicked pass in its id
// and make the description and comments visible
const { viewDescription } = this.state;
this.setState({ viewDescription: viewDescription === id ? -1 : id });
// add relevant comments to the state
var i;
var temp = [];
for (i = 0; i < this.props.comments.length; i++) {
if (this.props.comments[i].subject === id) {
temp.unshift(this.props.comments[i]);
}
}
this.setState({
comments: temp,
});
// save the subject id to local storage
// this is done incase a new comment is added
// then the subject associated with it can be retrieved
// and added as a property of that comment
localStorage.setItem("passedSubject", id);
//testing getUser
this.findAuthor(id); // this updates the tempUser in state
this.setState({ name: this.props.subAuthor.name });
};
// hovering on and off subjects toggles the visibility of the summary
hoverHandler = (id) => {
this.setState({ viewSummary: id });
};
hoverOffHandler = () => {
this.setState({ viewSummary: -1 });
};
rateHandler = (id, rate) => {
const subject = this.props.subjects.find((subject) => subject._id === id);
// when no subject was found, the updateSubject won't be called
subject &&
this.props.updateSubject(id, rate, subject.noOfVotes, subject.rating);
alert("Thank you for rating this subject.");
};
// take in the id of the subject
// find it in the props
// get its author id
// call the getUser passing the author id
findAuthor(id) {
console.log("Hitting findAuthor function");
const subject = this.props.subjects.find((subject) => subject._id === id);
const authorId = subject.author;
console.log(authorId);
this.props.getUser(authorId);
}
render() {
const subjectItems = this.props.subjects.map((subject) => {
// if the state equals the id set to visible if not set to invisible
var view = this.state.viewDescription === subject._id ? "" : "none";
var hover = this.state.viewSummary === subject._id ? "" : "none";
var comments = this.state.comments;
var subjectAuthor = this.state.name;
return (
<div key={subject._id}>
<div className="subjectTitle">
<p
className="title"
onClick={() => this.clickHandler(subject._id)}
onMouseEnter={() => this.hoverHandler(subject._id)}
onMouseLeave={() => this.hoverOffHandler()}
>
{subject.title}
</p>
<p className="rate">
Rate this subject:
<button onClick={() => this.rateHandler(subject._id, 1)}>
1
</button>
<button onClick={() => this.rateHandler(subject._id, 2)}>
2
</button>
<button onClick={() => this.rateHandler(subject._id, 3)}>
3
</button>
<button onClick={() => this.rateHandler(subject._id, 4)}>
4
</button>
<button onClick={() => this.rateHandler(subject._id, 5)}>
5
</button>
</p>
<p className="rating">
Rating: {(subject.rating / subject.noOfVotes).toFixed(1)}/5
</p>
<p className="summary" style={{ display: hover }}>
{subject.summary}
</p>
</div>
<div className="subjectBody " style={{ display: view }}>
<div className="subjectAuthor">
<p className="author">
Subject created by: {subjectAuthor}
<br /> {subject.date}
</p>
</div>
<div className="subjectDescription">
<p className="description">{subject.description}</p>
</div>
<div className="subjectLinks">Links:</div>
<div className="subjectComments">
<p style={{ fontWeight: "bold" }}>Comments:</p>
{comments.map((comment, i) => {
return (
<div key={i} className="singleComment">
<p>
{comment.title}
<br />
{comment.comment}
<br />
Comment by : {comment.author}
</p>
</div>
);
})}
<a href="/addcomment">
<div className="buttonAddComment">ADD COMMENT</div>
</a>
</div>
</div>
</div>
);
});
return (
<div id="Subject">
<GoogleSearch />
{subjectItems}
</div>
);
}
}
Subject.propTypes = {
fetchSubjects: PropTypes.func.isRequired,
fetchComments: PropTypes.func.isRequired,
updateSubject: PropTypes.func.isRequired,
getUser: PropTypes.func.isRequired,
subjects: PropTypes.array.isRequired,
comments: PropTypes.array.isRequired,
newPost: PropTypes.object,
subAuthor: PropTypes.object,
};
const mapStateToProps = (state) => ({
subjects: state.subjects.items,
newSubject: state.subjects.item,
comments: state.comments.items,
newComment: state.comments.item,
subAuthor: state.auth.tempUser[0],
});
// export default Subject;
export default connect(mapStateToProps, {
fetchSubjects,
fetchComments,
updateSubject, // rate subject
getUser, // used for getting author name
})(Subject, Comment);
I'd like to offer an alternative solution to the current code you have been writing so far. I know this is not codereview (and it wouldn't be on topic there, unless it is actually working code), but still, I would like to show you a different way of dividing up your components.
From what I see, you have many components, currently all jampacked in to one very large component. This can complicate things on the long run, and if you can, you should avoid it.
As I see it from the code you have posted, you really have several components, which I divided in:
Subject
Comment
User
Rating
RatingViewer
By dividing your now large component, you are making it easier to handle the data for one component at a later time and reuse the components you are making. You might want to reuse some of these components.
For the purpose of an alternative solution, I created a very quick and basic demo on how you might refactor your code. This is only a suggestion, in the hope that it will also solve your current problem.
The problem you are having is that you want to load that data, and use it directly. Any fetch operation is however asynchronous, so after you have called this.props.getUser(authorId); your author gets added somewhere in your state, but it will not be available until fetching has been completed and your component gets re-rendered.
I hope the information in the demo can give you some insight, it might not be exactly matching your scenario, but it should give you an indication of what you could do differently.
// imports
const { Component } = React;
const { Provider, connect } = ReactRedux;
const { render } = ReactDOM;
const { createStore, combineReducers } = Redux;
// some fake db data
const db = {
comments: [
{ id: 1, subject: 2, user: 2, comment: 'Interesting book' },
{ id: 2, subject: 2, user: 3, comment: 'Is interesting the only word you know, you twit' }
],
subjects: [
{
id: 1,
title: 'Some interesting title',
summary: 'Some interesting summary / plot point',
author: 2,
rate: 0,
noOfVotes: 0
},
{
id: 2,
title: 'Some less interesting title',
summary: 'Some more interesting summary / plot point',
author: 1,
rate: 5,
noOfVotes: 2
}
],
authors: [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Doe' }
],
users: [
{ id: 1, name: 'user 1' },
{ id: 2, name: 'user 2' },
{ id: 3, name: 'user 3' }
]
};
// reducers
const authorReducer = ( state = {}, action ) => {
switch (action.type) {
case 'author/add':
return { ...state, [action.payload.id]: action.payload };
default:
return state;
}
};
const userReducer = ( state = {}, action ) => {
switch (action.type) {
case 'user/add':
return { ...state, [action.payload.id]: action.payload };
default:
return state;
}
};
const subjectReducer = ( state = {}, action ) => {
switch (action.type) {
case 'subject/retrieved':
return Object.assign( {}, ...action.payload.map( subject => ({ [subject.id]: subject }) ) );
case 'subject/add':
return { ...state, [action.payload.id]: action.payload };
case 'subject/update':
const { id } = action.payload;
return { ...state, [id]: action.payload };
default:
return state;
}
};
const commentReducer = ( state = [], action ) => {
switch (action.type) {
case 'comment/retrieved':
return action.payload.slice();
case 'comments/add':
return [...state, action.payload ];
default:
return state;
}
};
// create the store
const store = createStore( combineReducers({
users: userReducer,
authors: authorReducer,
comments: commentReducer,
subjects: subjectReducer
}) );
// some promise aware fetch methods
const fakeFetch = (entity, filter = null) => {
const entities = db[entity];
return Promise.resolve( (filter ? entities.filter( filter ) : entities).map( e => ({...e}) ) );
}
const fakeUpdate = (entity, id, updatedValue ) => {
const targetEntity = db[entity].find( e => e.id === id );
if (!targetEntity) {
return Promise.reject();
}
Object.assign( targetEntity, updatedValue );
return Promise.resolve( { ...targetEntity } );
}
// separate components
class App extends Component {
render() {
return <Subjects />;
}
}
// subjects component
// cares about retrieving the subjects and displaying them
class SubjectsComponent extends Component {
componentDidMount() {
this.props.fetchSubjects();
}
render() {
const { subjects } = this.props;
if (!subjects || !subjects.length) {
return <div>Loading</div>;
}
return (
<div>
{ subjects.map( subject => <Subject key={subject.id} subject={subject} /> ) }
</div>
);
}
}
// subject component
// displays a subject and fetches the comments for "all" subjects
// this should probably only fetch its own comments, but then reducer has to be changed aswell
// to be aware of that
class SubjectComponent extends Component {
componentDidMount() {
this.props.fetchComments();
}
render() {
const { subject } = this.props;
return (
<div className="subject">
<h1>{ subject.title }<RateView subject={subject} /></h1>
<p>{ subject.summary }</p>
<Rate subject={subject} />
<h2>Comments</h2>
{ this.props.comments && this.props.comments.map( comment => <Comment key={comment.id} comment={comment} /> ) }
</div>
);
}
}
// Just displays a comment and a User component
const Comment = ({ comment }) => {
return (
<div className="comment">
<p>{ comment.comment }</p>
<User id={comment.user} />
</div>
);
}
// User component
// fetches the user in case he hasn't been loaded yet
class UserComponent extends Component {
componentDidMount() {
if (!this.props.user) {
this.props.fetchUser( this.props.id );
}
}
render() {
return <span className="user">{ this.props.user && this.props.user.name }</span>;
}
}
// shows the current rating of a post
const RateView = ({ subject }) => {
if (subject.noOfVotes === 0) {
return <span className="rating">No rating yet</span>;
}
const { rate, noOfVotes } = subject;
return <span className="rating">Total rating { (rate / noOfVotes).toFixed(1) }</span>;
}
// enables voting on a subject, can be triggered once per rendering
// this should truly be combined with the user who rated the subject, but it's a demo
class RateComponent extends Component {
constructor() {
super();
this.onRateClicked = this.onRateClicked.bind( this );
this.state = {
hasRated: false,
rateValue: -1
};
}
onRateClicked( e ) {
const userRate = parseInt( e.target.getAttribute('data-value') );
const { subject } = this.props;
this.setState({ hasRated: true, rateValue: userRate }, () => {
this.props.updateRate( { ...subject, rate: subject.rate + userRate, noOfVotes: subject.noOfVotes + 1 } );
});
}
render() {
if (this.state.hasRated) {
return <span className="user-rate">You rated this subject with { this.state.rateValue }</span>;
}
return (
<div>
{ [1, 2, 3, 4, 5].map( value => <button type="button" onClick={ this.onRateClicked } data-value={value} key={value}>{ value }</button> ) }
</div>
);
}
}
// connecting all the components to the store, with their states and dispatchers
const Subjects = connect(
state => ({ subjects: Object.values( state.subjects ) }),
dispatch => ({
fetchSubjects() {
return fakeFetch('subjects').then( result => dispatch({ type: 'subject/retrieved', payload: result }) );
}
}))( SubjectsComponent );
// ownProps will be used to filter only the data required for the component that it is using
const Subject = connect(
(state, ownProps) => ({ comments: state.comments.filter( comment => comment.subject === ownProps.subject.id ) }),
dispatch => ({
fetchComments() {
return fakeFetch('comments' ).then( result => dispatch({ type: 'comment/retrieved', payload: result }) );
}
}))( SubjectComponent );
const User = connect(
(state, ownProps) => ({ user: state.users[ownProps.id] }),
dispatch => ({
fetchUser( id ) {
return fakeFetch('users', user => user.id === id).then( result => dispatch({ type: 'user/add', payload: result[0] }) );
}
}))( UserComponent );
const Rate = connect( null, dispatch => ({
updateRate( updatedSubject ) {
return fakeUpdate('subjects', updatedSubject.id, updatedSubject).then( updated => dispatch({ type: 'subject/update', payload: updated }) );
}
}))( RateComponent );
// bind it all together and run the app
const targetElement = document.querySelector('#container');
render( <Provider store={store}><App /></Provider>, targetElement );
.user {
font-style: italic;
font-size: .9em;
}
.comment {
padding-left: 10px;
background-color: #efefef;
border-top: solid #ddd 1px;
}
h1, h2 {
font-size: .8em;
line-height: .9em;
}
.rating {
padding: 5px;
display: inline-block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js" integrity="sha512-SUJujhtUWZUlwsABaZNnTFRlvCu7XGBZBL1VF33qRvvgNk3pBS9E353kcag4JAv05/nsB9sanSXFbdHAUW9+lg==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js" integrity="sha512-SYsXmAblZhruCNUVmTp5/v2a1Fnoia06iJh3+L9B9wUaqpRVjcNBQsqAglQG9b5+IaVCfLDH5+vW923JL5epZA==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.1/react-redux.min.js" integrity="sha512-Ae6lzX7eAwqencnyfCtoAf2h3tQhsV5DrHiqExqyjKrxvTgPHwwOlM694naWdO2ChMmBk3by5oM2c3soVPbI5g==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js" integrity="sha512-P36ourTueX/PrXrD4Auc1kVLoTE7bkWrIrkaM0IG2X3Fd90LFgTRogpZzNBssay0XOXhrIgudf4wFeftdsPDiQ==" crossorigin="anonymous"></script>
<div id="container"></div>
I'm trying to fetch data through Redux (with actions & reducers) and store it in a ReactTable
Here is the Table :
// MisleadLeadsTable
import React from "react";
import "react-table-v6/react-table.css";
import ReactTable from "react-table-v6";
import { connect } from "react-redux";
import {
getLeadsNotValid,
updateSpecificNotValidLead
} from "../../actions/leads";
import Spinner from "../layout/Spinner";
class MisleadLeadsTable extends React.Component {
constructor(props) {
super();
const { getLeadsNotValid } = props;
// Going to get data from the Server
// Call the Action and use the Reducer
getLeadsNotValid();
// Later put the data in the state
this.state = {
data: []
};
this.renderEditable = this.renderEditable.bind(this);
}
componentDidMount() {
// TODO
const { leadsNotValid } = this.props;
this.setState({
data: leadsNotValid
});
}
// Edit the cells
renderEditable(cellInfo) {
return (
<div
style={{ backgroundColor: "#fafafa" }}
contentEditable
suppressContentEditableWarning
onBlur={e => {
const data = [...this.state.data];
data[cellInfo.index][cellInfo.column.id] = e.target.innerHTML;
this.setState({ data });
}}
dangerouslySetInnerHTML={{
__html: this.state.data[cellInfo.index][cellInfo.column.id]
}}
/>
);
}
render() {
// loading data or not
const { loadingData } = this.props;
// This "data" should hold the fetched data from the server
const { data } = this.state;
return (
<div>
{loadingData ? (
<Spinner />
) : (
<div>
<ReactTable
data={data}
columns={[
{
Header: "Business Name",
accessor: "BusinessName"
// Cell: this.renderEditable
}
]}
defaultPageSize={10}
className="-striped -highlight"
/>
<br />
</div>
)}
</div>
);
}
}
const mapStateToProps = state => ({
loadingData: state.leadReducer.loadingData,
leadsNotValid: state.leadReducer.leadsNotValid
});
const mapDispatchToProps = { getLeadsNotValid, updateSpecificNotValidLead };
export default connect(mapStateToProps, mapDispatchToProps)(MisleadLeadsTable);
However when I try to store the data in the State (in componentDidMount) it always comes back empty , and when the table is being rendered it gets an empty array.
It is crucial to store the data in the State because I'm trying to implement an editable table.
The data is stored in leadsNotValid , and if I do :
<ReactTable
data={leadsNotValid} // Notice !! Changed this
columns={[
{
Header: "Business Name",
accessor: "BusinessName"
// Cell: this.renderEditable
}
]}
defaultPageSize={10}
className="-striped -highlight"
/>
Then the data is presented successfully to the user , however it's not in the State of the component.
How can I put the leadsNotValid in the State using setState ?
Here are the Action & Reducer if it's needed (THEY WORK GREAT !) :
Action :
import axios from "axios";
import {
REQUEST_LEADS_NOT_VALID,
REQUEST_LEADS_NOT_VALID_SUCCESS,
UPDATED_SUCCESSFULLY_A_NOT_VALID_LEAD_THAT_NOW_IS_VALID,
UPDATE_A_SINGLE_NOT_VALID_LEAD
} from "./types";
export const updateSpecificNotValidLead = updatedLead => async dispatch => {
dispatch({
type: UPDATE_A_SINGLE_NOT_VALID_LEAD
});
const config = {
headers: {
"Content-Type": "application/json"
}
};
const body = JSON.stringify({ updatedLead });
const res = await axios.post(
".......API/Something1/....",
body,
config
);
if (res !== null && res.data !== null) {
dispatch({
type: UPDATED_SUCCESSFULLY_A_NOT_VALID_LEAD_THAT_NOW_IS_VALID
});
}
};
export const getLeadsNotValid = () => async dispatch => {
dispatch({
type: REQUEST_LEADS_NOT_VALID
});
const res = await axios.get(".......API/Something2/....");
if (res !== null && res.data !== null) {
dispatch({
type: REQUEST_LEADS_NOT_VALID_SUCCESS,
payload: res.data
});
}
};
Reducer :
import {
GET_MAIN_LEADS_SUCCESS,
REQUEST_MAIN_LEADS,
RELOAD_DATA_MAIN_LEADS_TABLE,
REQUEST_LEADS_NOT_VALID,
REQUEST_LEADS_NOT_VALID_SUCCESS,
UPDATE_A_SINGLE_NOT_VALID_LEAD,
UPDATED_SUCCESSFULLY_A_NOT_VALID_LEAD_THAT_NOW_IS_VALID
} from "../actions/types";
const initialState = {
mainLeadsClients: [],
loadingData: null, // general loader
reloadMainLeadTable: 0,
reloadMisleadTable: 0,
leadsNotValid: []
};
export default function(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case REQUEST_LEADS_NOT_VALID:
return {
...state,
loadingData: true
};
case REQUEST_LEADS_NOT_VALID_SUCCESS:
return {
...state,
loadingData: false,
leadsNotValid: payload
};
case UPDATE_A_SINGLE_NOT_VALID_LEAD:
return {
...state,
loadingData: true
};
case UPDATED_SUCCESSFULLY_A_NOT_VALID_LEAD_THAT_NOW_IS_VALID:
return {
...state,
reloadMisleadTable: state.reloadMisleadTable + 1,
loadingData: false
};
// ... more
default:
return state;
}
}
You may have to write super(props) instead of super() in order to access props in the constructor.
So, I have a property (fields), within which I wish to change the value of an element (countries). Alerting the value of countries currently displays the value 2, but I want to change the value to 100, so that re-alerting fields.countries.value, after the change, displays the new value.
How do I do this?
import type { State } from '../../common/types';
import DynamicField from './DynamicField';
import R from 'ramda';
import React from 'react';
import buttonsMessages from '../../common/app/buttonsMessages';
import linksMessages from '../../common/app/linksMessages';
import { FormattedMessage } from 'react-intl';
import { ValidationError } from '../../common/lib/validation';
import { connect } from 'react-redux';
import { fields } from '../../common/lib/redux-fields';
import {
Block,
Box,
Button,
Checkbox,
FieldError,
Flex,
Form,
Heading,
Input,
PageHeader,
Pre,
Radio,
Select,
Space,
Title,
View,
} from '../app/components';
// The example of dynamically loaded editable data.
// cato.org/publications/commentary/key-concepts-libertarianism
const keyConceptsOfLibertarianism = [
'Individualism',
'Individual Rights',
'Spontaneous Order',
'The Rule of Law',
'Limited Government',
'Free Markets',
'The Virtue of Production',
'Natural Harmony of Interests',
'Peace',
].map((concept, index) => ({
id: index,
name: concept,
}));
// Proof of concept. Country list will be read from firebase
const countryArray = [
{ label: 'Select Country', value: 0 },
{ label: 'France', value: 2 },
{ label: 'England', value: 4 },
{ label: 'Swizterland', value: 8 },
{ label: 'Germany', value: 16 },
{ label: 'Lithuania', value: 32 },
{ label: 'Romania', value: 64 },
].map((countryName, index) => ({
id: index,
name: countryName,
}));
// Dynamically create select list
const countryOptions = [];
countryArray.map(countryItem =>
countryOptions.push({ label: countryItem.name.label, value: countryItem.name.value }),
);
// Proof of concept. Country list will be read from firebase
const cityArray = [
{ label: 'Select City', value: 0 },
{ label: 'London', value: 50 },
{ label: 'Paris', value: 75 },
].map((cityName, index) => ({
id: index,
name: cityName,
}));
// Dynamically create select list
const cityOptions = [];
cityArray.map(cityItem =>
cityOptions.push({ label: cityItem.name.label, value: cityItem.name.value }),
);
// Proof of concept. Country list will be read from firebase
const gymArray = [
{ label: 'Select Gym', value: 0 },
{ label: 'Virgin Sport', value: 23 },
{ label: 'Sports Direct', value: 45 },
].map((gymName, index) => ({
id: index,
name: gymName,
}));
// Dynamically create select list
const gymOptions = [];
gymArray.map(gymItem =>
gymOptions.push({ label: gymItem.name.label, value: gymItem.name.value }),
);
type LocalState = {
disabled: boolean,
error: ?Object,
submittedValues: ?Object,
};
class FieldsPage extends React.Component {
static propTypes = {
fields: React.PropTypes.object.isRequired,
dynamicFields: React.PropTypes.object,
// getCities: React.PropTypes.object,
};
state: LocalState = {
disabled: false,
error: null,
submittedValues: null,
};
onFormSubmit = () => {
const { dynamicFields, fields } = this.props;
const values = {
...fields.$values(),
concepts: {
...dynamicFields,
},
};
// This is just a demo. This code belongs to Redux action creator.
// Disable form.
this.setState({ disabled: true });
// Simulate async action.
setTimeout(() => {
this.setState({ disabled: false });
const isValid = values.name.trim();
if (!isValid) {
const error = new ValidationError('required', { prop: 'name' });
this.setState({ error, submittedValues: null });
return;
}
this.setState({ error: null, submittedValues: values });
fields.$reset();
}, 500);
};
handleSelectedCountryChange = () => {
// Pass in the selected country value to get associated cites
const { fields, getCities } = this.props;
getCities('country', fields.$values());
};
/*
handleSelectedCityChange = (event => {
// Pass in the selected city value to get associated gyms
this.setState({secondLevel: event.target.value});
});
*/
render() {
const { fields } = this.props;
const { disabled, error, submittedValues } = this.state;
return (
<View>
<Title message={linksMessages.fields} />
<PageHeader
description="New clients enter their gym details here."
heading="New user entry form."
/>
<Form onSubmit={this.onFormSubmit}>
<Input
{...fields.name}
aria-invalid={ValidationError.isInvalid(error, 'name')}
disabled={disabled}
label="Your Name"
maxLength={100}
type="text"
/>
<FieldError error={error} prop="name" />
<Heading alt>Key Concepts of Libertarianism</Heading>
<Block>
<Flex wrap>
{keyConceptsOfLibertarianism.map(item =>
<Box mr={1} key={item.id}>
<DynamicField
disabled={disabled}
item={item}
path={['fieldsPage', 'dynamic', item]}
/>
</Box>,
)}
</Flex>
</Block>
<Block>
<Checkbox
{...fields.isLibertarian}
checked={fields.isLibertarian.value}
disabled={disabled}
label="I'm libertarian"
/>
<Checkbox
{...fields.isAnarchist}
checked={fields.isAnarchist.value}
disabled={disabled}
label="I'm anarchist"
/>
</Block>
<Block>
<Flex>
<Radio
{...fields.gender}
checked={fields.gender.value === 'male'}
disabled={disabled}
label="Male"
value="male"
/>
<Space x={2} />
<Radio
{...fields.gender}
checked={fields.gender.value === 'female'}
disabled={disabled}
label="Female"
value="female"
/>
<Space x={2} />
<Radio
{...fields.gender}
checked={fields.gender.value === 'other'}
disabled={disabled}
label="Other"
value="other"
/>
</Flex>
</Block>
<Block>
<Select
{...fields.countries}
disabled={disabled}
label="Countries"
onChange={this.handleSelectedCountryChange}
options={countryOptions}
/>
</Block>
<Block>
<Select
{...fields.cities}
disabled={disabled}
label="Cities"
// onChange={this.handleSelectedCityChange}
options={cityOptions}
/>
</Block>
<Block>
<Select
{...fields.gyms}
disabled={disabled}
label="Gyms"
// onChange={this.handleSelectedCityChange}
options={gymOptions}
/>
</Block>
{/*
Why no multiple select? Because users are not familiar with that.
Use checkboxes or custom checkable dynamic fields instead.
*/}
<Button disabled={disabled} type="submit">
<FormattedMessage {...buttonsMessages.submit} />
</Button>
{submittedValues &&
<Pre>
{JSON.stringify(submittedValues, null, 2)}
</Pre>
}
</Form>
</View>
);
}
}
FieldsPage = fields({
path: 'fieldsPage',
fields: [
'countries',
'cities',
'gyms',
'gender',
'isAnarchist',
'isLibertarian',
'name',
],
getInitialState: () => ({
countries: '0',
cities: '0',
gyms: '0',
gender: 'male',
isAnarchist: false,
isLibertarian: false,
}),
})(FieldsPage);
export default connect(
(state: State) => ({
dynamicFields: R.path(['fieldsPage', 'dynamic'], state.fields),
}),
)(FieldsPage);
=====================================================================
fields.js
/* #flow weak */
import R from 'ramda';
import React from 'react';
import invariant from 'invariant';
import { resetFields, setField } from './actions';
type Path = string | Array<string> | (props: Object) => Array<string>;
type Options = {
path: Path,
fields: Array<string>,
getInitialState?: (props: Object) => Object,
};
const isReactNative =
typeof navigator === 'object' &&
navigator.product === 'ReactNative'; // eslint-disable-line no-undef
// Higher order component for huge fast dynamic deeply nested universal forms.
const fields = (options: Options) => (WrappedComponent) => {
const {
path = '',
fields = [],
getInitialState,
} = options;
invariant(Array.isArray(fields), 'Fields must be an array.');
invariant(
(typeof path === 'string') ||
(typeof path === 'function') ||
Array.isArray(path)
, 'Path must be a string, function, or an array.');
return class Fields extends React.Component {
static contextTypes = {
store: React.PropTypes.object, // Redux store.
};
static getNormalizePath(props) {
switch (typeof path) {
case 'function': return path(props);
case 'string': return [path];
default: return path;
}
}
static getFieldValue(field, model, initialState) {
if (model && {}.hasOwnProperty.call(model, field)) {
return model[field];
}
if (initialState && {}.hasOwnProperty.call(initialState, field)) {
return initialState[field];
}
return '';
}
static lazyJsonValuesOf(model, props) {
const initialState = getInitialState && getInitialState(props);
// http://www.devthought.com/2012/01/18/an-object-is-not-a-hash
return options.fields.reduce((fields, field) => ({
...fields,
[field]: Fields.getFieldValue(field, model, initialState),
}), Object.create(null));
}
static createFieldObject(field, onChange) {
return isReactNative ? {
onChangeText: (text) => {
onChange(field, text);
},
} : {
name: field,
onChange: (event) => {
// Some custom components like react-select pass the target directly.
const target = event.target || event;
const { type, checked, value } = target;
const isCheckbox = type && type.toLowerCase() === 'checkbox';
onChange(field, isCheckbox ? checked : value);
},
};
}
state = {
model: null,
};
fields: Object;
values: any;
unsubscribe: () => void;
onFieldChange = (field, value) => {
const normalizedPath = Fields.getNormalizePath(this.props).concat(field);
this.context.store.dispatch(setField(normalizedPath, value));
};
createFields() {
const formFields = options.fields.reduce((fields, field) => ({
...fields,
[field]: Fields.createFieldObject(field, this.onFieldChange),
}), {});
this.fields = {
...formFields,
$values: () => this.values,
$setValue: (field, value) => this.onFieldChange(field, value),
$reset: () => {
const normalizedPath = Fields.getNormalizePath(this.props);
this.context.store.dispatch(resetFields(normalizedPath));
},
};
}
getModelFromState() {
const normalizedPath = Fields.getNormalizePath(this.props);
return R.path(normalizedPath, this.context.store.getState().fields);
}
setModel(model) {
this.values = Fields.lazyJsonValuesOf(model, this.props);
options.fields.forEach((field) => {
this.fields[field].value = this.values[field];
});
this.fields = { ...this.fields }; // Ensure rerender for pure components.
this.setState({ model });
}
componentWillMount() {
this.createFields();
this.setModel(this.getModelFromState());
}
componentDidMount() {
const { store } = this.context;
this.unsubscribe = store.subscribe(() => {
const newModel = this.getModelFromState();
if (newModel === this.state.model) return;
this.setModel(newModel);
});
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
return (
<WrappedComponent {...this.props} fields={this.fields} />
);
}
};
};
export default fields;