component does not display updated redux state - javascript

I have a Meals page with meal cards that toggles a modal with details about the clicked meal when the card is clicked:
{
this.props.filteredMeals.map(m => {
return (
<div onClick={() => this.props.toggleMealModal(m)} key={m.id}>
<MealCard meal={m} />
</div>
)
})
}
toggleMealModal(meal) updates the meal inside my redux store.
meal: {
description: "Marre des sashimis de thon ou de saumon ? Tentez le maquereau!",
fees: "80",
id: 27,
ingredients: "maquereau cru",
name: "Sashimi de maquereau",
no_fees_price: 599,
original_price: 850,
}
inside my MealModal I have a PaymentContainer child component that also mounts when the modal is toggle and calls an action:
componentDidMount() {
this.props.getUserCredit(this.props.meal.no_fees_price, this.props.meal.fees);
}
where this.props.meal is fetched from my redux store via mapStateToProps.
the getUserCredit() is the following action:
export const getUserCredit = (noFeesPrice, fees) => {
return dispatch => {
axios.get('/get_user_credit')
.then(res => {
dispatch(setUserCredit(parseInt(res.data.credit) / 100));
parseInt(noFeesPrice) + parseInt(fees) - parseInt(res.data.credit) < 0 ?
dispatch(setMealPrice(0, (parseInt(noFeesPrice) + parseInt(fees)) / 100))
:
dispatch(setMealPrice((parseInt(noFeesPrice) + parseInt(fees) - parseInt(res.data.credit)) / 100, parseInt(res.data.credit) / 100)
);
})
.catch(err => console.log(err));
}
}
and then a child component displays the mealPrice, fetched from the redux store (and supposedly updated by getUserCredit()).
The problem is: the very first clicked meal card displays nothing at this.props.mealPrice, and when other cards are clicked and the modal mounts, this.props.mealPrice displays the price from the previously clicked meal card.
How can I change my code so that the right price mounts with the right meal ?
EDIT: relevant reducers and action creator code :
export const setUserCredit = (credit = 0) => ({
type: SET_USER_CREDIT,
payload: { credit }
});
export const setMealPrice = (mealPrice = null, amountOff = 0) => ({
type: SET_MEAL_PRICE,
payload: { mealPrice, amountOff }
});
case SET_USER_CREDIT: {
return {
...state,
credit: action.payload.credit
}
}
case SET_MEAL_PRICE: {
return {
...state,
amountOff: action.payload.amountOff,
mealPrice: action.payload.mealPrice
}
}

I recommend you to use status indicators while working with APIs.
Very simplified, you could build some action creators like:
const getUserCreditStart = () => ({
type: GET_USER_CREDIT_START,
});
const getUserCreditSuccess = () => ({
type: GET_USER_CREDIT_SUCCESS,
});
const getUserCreditError = payload => ({
type: GET_USER_CREDIT_ERROR,
payload,
});
In your getUserCredit function you can then dispatch these actions accordingly:
export const getUserCredit = (noFeesPrice, fees) => (dispatch) => {
dispatch(getUserCreditsStart());
return axios.get('/get_user_credit')
.then(res => {
dispatch(setUserCredit(parseInt(res.data.credit) / 100));
parseInt(noFeesPrice) + parseInt(fees) - parseInt(res.data.credit) < 0
? dispatch(setMealPrice(0, (parseInt(noFeesPrice) + parseInt(fees)) / 100))
: dispatch(setMealPrice((parseInt(noFeesPrice) + parseInt(fees) - parseInt(res.data.credit)) / 100, parseInt(res.data.credit) / 100));
})
.then(() => dispatch(getUserCreditSuccess()))
.catch(err => dispatch(getUserCreditError(err)))
}
And then, you need to add them to a reducer. Btw. typeToReducer might be a good choice here. :)
In the reducer you need to set the status on the different actions that got dispatched.
const initialState = {
status: 'INITIAL',
};
...
case GET_USER_CREDIT_START: {
return {
...state,
status: 'START',
}
}
case GET_USER_CREDIT_SUCCESS: {
return {
...state,
status: 'SUCCESS',
}
}
case GET_USER_CREDIT_ERROR: {
return {
...state,
status: 'ERROR',
error: action.payload,
}
}
Well, and in your PaymentContainer component you then can wait for the answer and show a loading bar while you are waiting (the status information you get with mapStateToProps like you do with the results). In case of an error, you are able to display the error as well.

Related

React redux - useSelect() and useDispatch() when mapping objects

//state.buttons is an array - it is used to map buttons to a menu
function NavMenu() {
const buttons = useSelector((state) => state.buttons);
const dispatch = useDispatch();
const navButtons = buttons.map((button) => {
const checkDirection = (button) => {
if (button.pageNo >= button.prevPageNo) {
dispatch(updateButtonsData({ newDirection: 1 }));
} else {
dispatch(updateButtonsData({ newDirection: -1 }));
}
};
checkDirection(button);
return (
<li key={button.id} >
<NavButton buttonData={button} />
</li>
);
});
return <ul>{navButtons}</ul>;
}
export default NavMenu;
//buttonSlice
const initialState = [
{
id: 'homeId',
prevPageNo: null,
pageNo: 1,
direction: 1,
firstAnimFrame: 1,
lastAnimFrame: 1,
},
{
id: 'aboutId',
prevPageNo: null,
pageNo: 2,
direction: 1,
firstAnimFrame: 1,
lastAnimFrame: 11,
},{...}
const buttonsSlice = createSlice({
name: 'ui',
initialState,
reducers: {
updateButtonsData: {
reducer(state, action) {
state = action.payload;
},
prepare(
id,
newPrevPageNo,
pageNo,
newDirection,
newFirstFrame,
newLastFrame
) {
return {
payload: {
id,
prevPageNo: newPrevPageNo,
pageNo,
direction: newDirection,
firstAnimFrame: newFirstFrame,
lastAnimFrame: newLastFrame,
};},},},},});
Hello, I have a navigation menu where several parameters must be dynamically updated within the UI when a user presses a button. Each button will behave differently depending on the previous button as pressed (order of pressed buttons matters). My solution is to compare the initial state of the nav buttons array to each subsequent remapping. I thought it would be good to store the history of the previously pressed button as a value inside the nav objects array. Each time the navigation is refreshed, new parameters are added and accessible as props inside the mapped button and can then be accessed onClick.
Currently, the dispatch function does not work with my code, and the mapped buttons array remains unchanged.
I assume that you are "triggering" the update data via checkDirection(button); and not onClick Event.
first this is a "no-go" code:
const navButtons = buttons.map((button) => {
const checkDirection = (button) => {
if (button.pageNo >= button.prevPageNo) {
dispatch(updateButtonsData({ newDirection: 1 }));
} else {
dispatch(updateButtonsData({ newDirection: -1 }));
}
};
checkDirection(button);
return (
<li key={button.id} >
<NavButton buttonData={button} />
</li>
);
});
return <ul>{navButtons}</ul>;
}
If you want to trigger a function while rendering an element, put it inside useEffect and not "outside".
So, transfer that code inside NavButton like:
const checkDirection = useCallback(button) => {
if (button.pageNo >= button.prevPageNo) {
dispatch(updateButtonsData({ newDirection: 1 }));
} else {
dispatch(updateButtonsData({ newDirection: -1 }));
}
}, []);
useEffect(() => checkDirection(button), [checkDirection, button]);

Not able to add todo in a todo-list using react and redux. What am I doing wrong?

render() {
const listItems = this.props.todos.map((todo) =>
<ListItem key={todo.id} id={todo.id} content={todo.content} onEdit={this.onEditItem}/>
)
return <>
<ul className="todo-list">
{listItems}
</ul>
{/* <AddItem/> */}
<div className="add-item">
<input type="text" onChange={this.onChangeValue}/>
<button type="submit" onClick={this.onAddItem}>Add Item</button>
</div>
</>
}
onAddItem = () => {
this.props.submitNewTodo({ id: this.props.todos.length + 1, content: this.state.value})
};
When I console.log this.props.todos.length it returns the value 2 and this.state.value returns the value typed into the input. But the "Add Item" button doesn't work.
I have mapped submitNewTodo to dispatch addTodo(newTodo) like so
const mapDispatchToProps = (dispatch) => {
return {
submitNewTodo: function(newTodo) {
dispatch(addTodo(newTodo));
}
}
}
Complete code is in this codepen.
https://codepen.io/blenderous/pen/MWjdyoN?editors=0011
Your addTodo action creator is wrong:
const addTodo = (todo) => {
type: 'ADD_TODO',
todo
};
this is a method that treats
type: 'ADD_TODO',
todo
as a method body. (type being used as the break label for the string 'ADD_TODO', followed by todo)
If you want to return an action, these two notations are correct:
const addTodo = (todo) => {
return {
type: 'ADD_TODO',
todo
}
};
// note the parantheses!
const addTodo = (todo) => ({
type: 'ADD_TODO',
todo
});
The first thing I notice with your code is that your reducer is not following the pattern Redux uses.
const todoReducer = ( state = [{ id: 1, content: "Call Client" },
{ id: 2, content: "Write Log" }], action ) => {
if (action.type == ADD_TODO) {
return state.concat(action.todo);
}
else {
return state;
}
}
The first rule that it breaks is that this should be a switch, not an if statement.
switch (action.type) {
case 'ADD_TODO':
// create new todos with the added todo
const newTodos = [
...state.todos,
action.payload.todo,
]
// new state object
return {
...state,
todos: newTodos,
}
default:
return {
...state,
}
}
The second rule is that you want to always have a payload property to follow the proper flux patterns. That payload would contain all of your data.
const addTodo = (todo) => {
type: 'ADD_TODO',
payload: {
todo,
}
};

Redux helper function to check if a value is in the store

I have a filter panel where the user can select different color filters:
// ColorButton.jsx
function ColorButton({ color }) {
const handleFilterSelected = ({ id }) => {
dispatch(applyFilter({ type: "colors", value: id }));
};
return (
<div className="color-button" onClick={() => handleFilterSelected(color)}>
{isFilterSelected({ type: "colors", value: color.id }) ? "yay" : "nay"}
</div>
);
}
The selected filters are stored in the redux store and the isFilterSelect function looks like this:
// redux/utils/filter.utils.js
export const isFilterSelected = ({ type, value }) => {
const {
filters: { applied }
} = store.getState();
return applied
.filter((f) => f.type === type)
.map((f) => f.value)
.includes(value);
};
The issue is that the check runs before a selected filter is added to the applied array.
As a more general question - is it even a good idea to have "helper" functions that depend on the redux store?
Your helper function there should be written as a selector that gets the current state, and you should be using useSelector instead of store.getState manually, as that will update your component when the selector value changes.
function ColorButton({ color }) {
const isSelected = useSelector(state => isFilterSelected(state, { type: "colors", value: color.id }));
return (
<div className="color-button">
{isSelected ? "yay" : "nay"}
</div>
);
}
// redux/utils/filter.utils.js
export const isFilterSelected = (state, { type, value }) => {
return state.filters.applied
.filter((f) => f.type === type)
.map((f) => f.value)
.includes(value);
};

Unable to update state

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>

React setState inside solidity contract instance promises

I deployed a solidity contract to my local testrpc blockchain. All my contract method tests check out, but handling Web3 transactions and updating state accordingly is giving me trouble.
When I add a user account, my next operation is to return all user accounts for my contract. and well...update my state (RegisteredAccounts).
However, through my chain of promises I'm not seeing my states update. I understand setState is asynchronous too, so how can I see my states update without refreshing the page or calling ComponentDidMount()?
Here is my Solidity Accounts Contract (the parts that I've handled so far
pragma solidity ^ 0.4.4;
contract Accounts {
mapping(address => User) public mUsers;
address[] public Users; //users whitepages
struct User {
string handle;
bytes32[] taskList;
}
function addNewUser(string _handle) returns(bool success) {
address newUserAddr = msg.sender;
//if handle not in userAddresses & the handle is not null
if (bytes(mUsers[newUserAddr].handle).length == 0 && bytes(_handle).length != 0) {
mUsers[newUserAddr].handle = _handle;
Users.push(newUserAddr);
return true;
} else {
return false;
}
}
function getUsers() constant returns(address[]) {
return Users;
}
}
Here is my App container component -- relevant parts
registerNewUser() is my problem child right now.
class App extends Component {
state = {
modalOpen: false,
SenderAddress: null,
RegisteredAccounts: [],
isRegisteredUser: false,
SenderTaskList: [], //not set
AccountsCtrct: null,
web3: null
}
//#region APP METHODS
componentWillMount() {
// Get network provider and web3 instance. -- See utils/getWeb3 for more info.
getWeb3.then(results => {
this.setState({
web3: results.web3
})
this.instantiateContracts() //instantiate contract
}).catch(() => {
console.log('Error finding web3.')
})
}
instantiateContracts() {
this.setState({
AccountsCtrct: contract(AccountsContract)
})
this.state.AccountsCtrct.setProvider(this.state.web3.currentProvider)
//Get block chain addresses --- only returns the current address selected in metamask (web3 current addr)
this.state.web3.eth.getAccounts((error, accounts) => {
this.setState({
SenderAddress: accounts[0]
})
//INIT ACCOUNTS CONTRACT
var acctDeployed = this.state.AccountsCtrct.deployed()
acctDeployed.then((instance) => {
return instance.getUsers();
}).then((res) => {
this.setState({
RegisteredAccounts: res
})
if (this.state.RegisteredAccounts.includes(this.state.SenderAddress)) {
this.setState({
isRegisteredUser: true
})
}
})
})
}
registerUser = (handle) => {
var acctInstance
this.state.AccountsCtrct.deployed().then((inst) => {
//add current user to this account
acctInstance = inst
return acctInstance.addNewUser(handle, {
from: this.state.SenderAddress
});
}).then(() => {
//now we added our user -- update registeredAccounts setState
//pass response users array to promise
return acctInstance.getUsers()
}).then(res => {
this.setState({
RegisteredAccounts: res
})
if (res.includes(this.state.SenderAddress)) {
this.setState({
isRegisteredUser: true
})
}
})
}
toggleModal = () => {
this.setState(prevState => ({
modalOpen: !prevState.modalOpen
}));
}
//#endregion
render() {
return (
<div className="App">
<nav className="navbar pure-menu pure-menu-horizontal">
Truffle Box
{
!this.state.isRegisteredUser
? <a style={navLink} onClick={ this.toggleModal } href="#" className="pure-menu-heading pure-menu-link">Register</a>
: null
}
</nav>
<ModalUserNav visible={this.state.modalOpen}
toggleModal={this.toggleModal}
isRegistered={this.state.isRegisteredUser}
registerUser={this.registerUser} />
);
}
}
Last my Child component
class ModalUserNav extends Component {
state = {
unpl: "UserName",
pwpl: "Password",
errorCode: 'Registration Failed',
errorVisible: false
}
handleOnChangePL = (e) => {
this.setState({
[e.target.name]: e.target.value
})
}
handleSubmit = () => {
if (this.state.unpl !== "") {
this.props.registerUser(this.state.unpl)
this.props.toggleModal();
} else {
//if the input is empty update the error code and show
console.log('registration failed!')
this.setState({
errorCode: 'REGISTRATION ERR: empty handles are not allowed!',
errorVisible: true
})
}
}
render() {
return (
<section>
<Modal visible={this.props.visible} effect="fadeInUp">
<div className="pure-form">
<fieldset style={modalFormView}>
<legend style={{fontSize: "18px"}}><b>Register now. All you need is a handle!</b></legend>
<div className="flexContainer">
<input style={{marginTop: "7px", height: "2.6em", marginLeft: "5px", marginRight: "5px"}} type="text" name="unpl" placeholder={this.state.unpl} onChange={(event) => {this.handleOnChangePL(event)}} value={this.state.unpl} />
<button style={btnStyle} type="submit" className="pure-button pure-button-primary" onClick={() => {this.handleSubmit()}}><b>Register</b></button>
</div>
</fieldset>
</div>
</Modal>
</section>
)
}
}
In short, I want to follow up my 2 asynchronous tasks (addNewUser, getUsers) with a setState so I can automatically change my UI without refreshing. So what am I doing wrong?
You should move instantiateContracts to setState because setState does not update data immediately. https://reactjs.org/docs/react-component.html#setstate
this.setState({
web3: results.web3
}, () => {
this.instantiateContracts() //instantiate contract
})
Update 1: About registerUser: It should be
this.setState({
RegisteredAccounts: res
}, () => {
if (res.includes(this.state.SenderAddress)) {
this.setState({
isRegisteredUser: true
})
}
})

Categories

Resources