mapStateToProps value lags behind getState() by a keystroke - reactjs

I am using redux's connect() on App.js and then passing down a dispatcher to a child. The child also receives the state variable as a prop.
The issue is a log statement on the state variable through the prop mapping lags 1 digit behind what is entered. Here are 2 log statements showing this:
SelectDonation.js:19 getState value {donation_amount: "345"}
SelectDonation.js:20 mapstatetoprops value 34
Here is the child component SelectDonation.js:
handleInputChange = inputEvent => {
this.props.dispatch_set_donation_amount(inputEvent.target.value);
console.log("getState value", store.getState());
console.log("mapstatetoprops value", this.props.donation_amount)
};
render() {
return (
<React.Fragment>
<Form>
<input
type='number'
placeholder='Custom Amount'
name='donation_amount'
id='custom_amount'
onChange={(e) => this.handleInputChange(e)}
/>
<Button
primary
onClick={(event) => {
event.preventDefault();
this.props.dispatchChangeCheckoutStep(checkoutSteps.paymentDetails);
console.log(store.getState().checkoutStep)
}}>Next Step
</Button>
</Form>
</React.Fragment>
)
}
}
App.js:
class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<Modal
trigger={<Button color='purple'>Donate</Button>}
size='small'
>
{this.props.checkoutStep === checkoutSteps.selectDonation &&
<SelectDonation
dispatch_set_donation_amount = {this.props.dispatch_set_donation_amount}
dispatchChangeCheckoutStep={this.props.dispatchChangeCheckoutStep}
{...this.props} // passes down all the state
/>
}
{this.props.checkoutStep === checkoutSteps.paymentDetails && <PaymentDetails
redux_donation_amount={this.props.redux_donation_amount}
{...this.props}
/>
}
{this.props.checkoutStep === checkoutSteps.referrals && <Referrals dispatchUpdateStateData={this.props.dispatchUpdateStateData} />}
</Modal>
</header>
</div>
);
}
}
const map_state_to_props = (state) => {
return {
log_prop : state.log_to_console,
donation_amount : state.donation_amount,
checkoutStep : state.checkoutStep,
}
};
const map_dispatch_to_props = (dispatch, own_props) => {
return {
dispatch_set_donation_amount : amount => dispatch(set_donation_amount(amount)),
dispatchChangeCheckoutStep : newStep => dispatch(changeCheckoutStep(newStep)),
dispatchUpdateStateData : (stateData, stateVariable) => (dispatch(updateStateData(stateData, stateVariable)))
}
};
//connecting redux
export const AppWrapped = connect(map_state_to_props, map_dispatch_to_props)(App);
It looks correct to me but obviously I am neglecting something. What is causing the mapStateToProps variable to be one keystroke behind persistently?

The component has not updated yet. You have called an action to be dispatched to your reducer, but not waited for the component to receive the new props. You can see this by either pushing the second log into a setTimeout (not recommended)
this.props.dispatch_set_donation_amount(inputEvent.target.value);
console.log("getState value", store.getState());
setTimeout(() => console.log("mapstatetoprops value", this.props.donation_amount));
or by using a middleware such as thunk and returning a Promise from your action creator.
this.props.dispatch_set_donation_amount(inputEvent.target.value).then(x => console.log("mapstatetoprops value", x));
console.log("getState value", store.getState());

Because React has not had a chance to update the component. Your own code is still executing, so the rest of the component lifecycle has not executed yet, and therefore your component still has its existing props.
Also, please don't access the store directly in your components. One of the main purposes of connect is to handle that for you.

Related

React, passed state is not changing in other component

I started with react and have a small problem
export default function TestComponent3({ typeId}) {
console.log('CHANGING HERE', { typeId});
const handleClick = () => {
console.log('NOT CHANGING HERE', { typeId});
console.log('NOT CHANGING HERE EITHER', typeId);
};
return (
<>
<Spectrum.Button variant="secondary" onClick={handleClick}>
CHANGING HERE {typeId}
</Spectrum.Button>
</>
);
}
In the other component the state of 'myProp' is changing by the UI dropdown.
Spectrum.Button content changes dynamically but console logs are stuck on default state option.
I probably messed something up and and It's easy fix.
EDIT://
Sibling with a dropdown
export default function TypeForm({ typeId, setTypeId }) {
return (
<div>
<Spectrum.Dropdown className="dropdown" placeholder="Choose type...">
<Spectrum.Menu onChange={setTypeId} slot="options">
{Types.map(type => { //types 0,1,2,3,4,5,6,7,8
return (
<Spectrum.MenuItem selected={typeId === type.id ? true : null} key={type.id} className="jsontest">
{type.name}
</Spectrum.MenuItem>
);
})}
</Spectrum.Menu>
</Spectrum.Dropdown>
)}
</div>
);
}
And Parent
export default function Form() {
const [typeId, setTypeId] = useState(0);
const setTypeIdFunc = e => {
setTypeId(e.target.selectedIndex);
};
return (
<TestComponent3 typeId={typeId} />
<TypeForm
typeId={typeId}
setTypeId={setTypeIdFunc}
/>
)
The props are passed down first from the parent component of TestComponent3 during the first render.
This means you would have to send handleClick from one "higher" component. Or use a shared state-like context provider.
I fixed it by changing Spectrum.Button to normal button.

Losing state between renders if component is defined in another component

codesandbox here: https://codesandbox.io/s/restless-haze-v01wv?file=/src/App.js
I have a Users component which (when simplified) looks something like this:
const Users = () => {
const [toastOpen, setToastOpen] = useState(false)
// functions to handle toast closing
return (
<EditUser />
<Toast />
)
}
const EditUser = () => {
[user, setUser] = useState(null)
useEffect(() => {
const fetchedUser = await fetchUser()
setUser(fetchedUser)
}, [])
// this approach results in UserForm's username resetting when the toast closes
const Content = () => {
if (user) return <UserForm user={user} />
else return <div>Loading...</div>
}
return <Content />
// if I do this instead, everything's fine
return (
<div>
{
user ? <UserForm user={user} /> : <div>Loading...</div>
}
</div>
)
}
const UserForm = ({ user }) => {
const [username, setUsername] = useState(user.name)
return <input value={username}, onChange={e => setUsername(e.target.value)} />
}
While viewing the UserForm page while a Toast is still open, the UserForm state is reset when the Toast closes.
I've figured out that the issue is the Content component defined inside of EditUser, but I'm not quite clear on why this is an issue. I'd love a walkthrough of what's happening under React's hood here, and what happens in a "happy path"
You have defined Content inside EditUser component which we never do with React Components, because in this situtaion, Content will be re-created every time the EditUser is re-rendered. (surely, EditUser is going to be re-rendered few/many times).
So, a re-created Content component means the old Content will be destroyed (unmounted) and the new Content will be mounted.
That's why it is be being mounted many times and hence resetting the state values to initial values.
So, the solution is to just define it (Content) outside - not inside any other react component.
The culprit was EditUser's Content function, which predictably returns a brand new instance of each time it's called.

How to render a different component with React Hooks

I have a parent component with an if statement to show 2 different types of buttons.
What I do, on page load, I check if the API returns an array called lectures as empty or with any values:
lectures.length > 0 ? show button A : show button B
This is the component, called main.js, where the if statement is:
lectures.length > 0
? <div onClick={() => handleCollapseClick()}>
<SectionCollapse open={open} />
</div>
: <LectureAdd dataSection={dataSection} />
The component LectureAdd displays a + sign, which will open a modal to create a new Lecture's title, while, SectionCollapse will show an arrow to show/hide a list of items.
The logic is simple:
1. On page load, if the lectures.lenght > 0 is false, we show the + sign to add a new lecture
OR
2. If the lectures.lenght > 0 is true, we change and show the collpase arrow.
Now, my issue happens when I add the new lecture from the child component LectureAdd.js
import React from 'react';
import { Form, Field } from 'react-final-form';
// Constants
import { URLS } from '../../../../constants';
// Helpers & Utils
import api from '../../../../helpers/API';
// Material UI Icons
import AddBoxIcon from '#material-ui/icons/AddBox';
export default ({ s }) => {
const [open, setOpen] = React.useState(false);
const [ lucturesData, setLecturesData ] = React.useState(0);
const { t } = useTranslation();
const handleAddLecture = ({ lecture_title }) => {
const data = {
"lecture": {
"title": lecture_title
}
}
return api
.post(URLS.NEW_COURSE_LECTURE(s.id), data)
.then(data => {
if(data.status === 201) {
setLecturesData(lucturesData + 1) <=== this doesn't trigger the parent and the button remains a `+` symbol, instead of changing because now `lectures.length` is 1
}
})
.catch(response => {
console.log(response)
});
}
return (
<>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
<AddBoxIcon />
</Button>
<Form
onSubmit={event => handleAddLecture(event)}
>
{
({
handleSubmit
}) => (
<form onSubmit={handleSubmit}>
<Field
name='lecture_title'
>
{({ input, meta }) => (
<div className={meta.active ? 'active' : ''}>
<input {...input}
type='text'
className="signup-field-input"
/>
</div>
)}
</Field>
<Button
variant="contained"
color="primary"
type="submit"
>
ADD LECTURE
</Button>
</form>
)}
</Form>
</>
)
}
I've been trying to use UseEffect to trigger a re-render on the update of the variable called lucturesData, but it doesn't re-render the parent component.
Any idea?
Thanks Joe
Common problem in React. Sending data top-down is easy, we just pass props. Passing information back up from children components, not as easy. Couple of solutions.
Use a callback (Observer pattern)
Parent passes a prop to the child that is a function. Child invokes the function when something meaningful happens. Parent can then do something when the function gets called like force a re-render.
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((lecture) => {
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Call API
let lecture = callApi();
// Notify parent of event
onLectureCreated(lecture);
}, [onLectureCreated]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Similar to solution #1, except for Parent handles API call. The benefit of this, is the Child component becomes more reusable since its "dumbed down".
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((data) => {
// Call API
let lecture = callApi(data);
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Create lecture data to send to callback
let lecture = {
formData1: '',
formData2: ''
}
// Notify parent of event
onCreateLecture(lecture);
}, [onCreateLecture]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Use a central state management tool like Redux. This solution allows any component to "listen in" on changes to data, like new Lectures. I won't provide an example here because it's quite in depth.
Essentially all of these solutions involve the same solution executed slightly differently. The first, uses a smart child that notifies its parent of events once their complete. The second, uses dumb children to gather data and notify the parent to take action on said data. The third, uses a centralized state management system.

Accessing Apollo's loading boolean outside of Mutation component

The Mutation component in react-apollo exposes a handy loading boolean in the render prop function which is ideal for adding loaders to the UI whilst a request is being made. In the example below my Button component calls the createPlan function when clicked which initiates a GraphQL mutation. Whilst this is happening a spinner appears on the button courtesy of the loading prop.
<Mutation mutation={CREATE_PLAN}>
{(createPlan, { loading }) => (
<Button
onClick={() => createPlan({ variables: { input: {} } })}
loading={loading}
>
Save
</Button>
)}
</Mutation>
The issue I have is that other aspects of my UI also need to change based on this loading boolean. I have tried lifting the Mutation component up the React tree so that I can manually pass the loading prop down to any components which rely on it, which works, but the page I am building has multiple mutations that can take place at any given time (such as deleting a plan, adding a single item in a plan, deleting a single item in a plan etc.) and having all of these Mutation components sitting at the page-level component feels very messy.
Is there a way that I can access the loading property outside of this Mutation component? If not, what is the best way to handle this problem? I have read that you can manually update the Apollo local state using the update function on the Mutation component (see example below) but I haven't been able to work out how to access the loading value here (plus it feels like accessing the loading property of a specific mutation without having to manually write it to the cache yourself would be a common request).
<Mutation
mutation={CREATE_PLAN}
update={cache => {
cache.writeData({
data: {
createPlanLoading: `I DON"T HAVE ACCESS TO THE LOADING BOOLEAN HERE`,
},
});
}}
>
{(createPlan, { loading }) => (
<Button
onClick={() => createPlan({ variables: { input: {} } })}
loading={loading}
>
Save
</Button>
)}
</Mutation>
I face the same problem in my projects and yes, putting all mutations components at the page-level component is very messy. The best way I found to handle this is by creating React states. For instance:
const [createPlanLoading, setCreatePLanLoading] = React.useState(false);
...
<Mutation mutation={CREATE_PLAN} onCompleted={() => setCreatePLanLoading(false)}>
{(createPlan, { loading }) => (
<Button
onClick={() => {
createPlan({ variables: { input: {} } });
setCreatePLanLoading(true);
}
loading={loading}
>
Save
</Button>
)}
</Mutation>
I like the answer with React States. However, when there are many different children it looks messy with so many variables.
I've made a bit update for it for these cases:
const Parent = () => {
const [loadingChilds, setLoading] = useState({});
// check if at least one child item is loading, then show spinner
const loading = Object.values(loadingChilds).reduce((t, value) => t || value, false);
return (
<div>
{loading ? (
<CircularProgress />
) : null}
<Child1 setLoading={setLoading}/>
<Child2 setLoading={setLoading}/>
</div>
);
};
const Child1 = ({ setLoading }) => {
const [send, { loading }] = useMutation(MUTATION_NAME);
useEffect(() => {
// add info about state to the state object if it's changed
setLoading((prev) => (prev.Child1 !== loading ? { ...prev, Child1: loading } : prev));
});
const someActionHandler = (variables) => {
send({ variables});
};
return (
<div>
Child 1 Content
</div>
);
};
const Child2 = ({ setLoading }) => {
const [send, { loading }] = useMutation(MUTATION_NAME2);
useEffect(() => {
// add info about state to the state object if it's changed
setLoading((prev) => (prev.Child2 !== loading ? { ...prev, Child2: loading } : prev));
});
const someActionHandler = (variables) => {
send({ variables});
};
return (
<div>
Child 2 Content
</div>
);
};

child component does not re-render when props change

Problem:
The first page of my app shows list of Sale Invoices.
I select one which runs a function taking id of the clicked invoice as argument and gets its details via API call.
Customer component rendered from SaleInvoice has a input box (Typeahead component) which is supposed to show customerName passed down from SaleInvoice but does not do so correctly. It is sometimes blank and when I go back to the first page (list of Sale Invoices) and select other Sale Invoice, the customerName of the previous Sale Invoice. I checked the console log (see the line after <React.Fragment> in Customer component) and I can see the correct values of customerName in the state shown by reducers.
Initially, SaleInvoice was a stateful component but had no state object. I made it stateful to fetch my data via API in componentWillMount. Due to the above problem, I tried this:
added customerName in state in SaleInvoice
changed this.props.customerName to this.state.customerName in Customer props
used
getDerivedStateFromProps()
which says I cannot use componentWillMount
Also, tried shouldComponentUpdate and some other stuff.
Nothing works. Kindly help. If more code needs to be posted, please let me know.
relevant slice of reducer
case actionTypes.INVOICE_BY_ID_SUCCESS:
let customerData = action.payload[0];
let saleInvoiceData = action.payload[1];
let newState = Object.assign({}, state);
newState.loading = false;
newState.error = null;
newState.customerInfo = {
...state.customerInfo,
id: customerData.id,
place: customerData.place,
addressLineOne: customerData.address_line_one,
};
newState.saleInvoiceId = saleInvoiceData.id;
newState.customerName = saleInvoiceData.customer_name;
newState.serialNumber = saleInvoiceData.serial_number;
newState.amountBeforeFreight = saleInvoiceData.amount_before_freight;
newState.freight = saleInvoiceData.freight;
newState.amountAfterFreight = saleInvoiceData.amount_after_freight;
return newState;
SaleInvoiceContainer.js (excluding imports)
const mapStateToProps = (state, ownProps) => {
console.log(`ownProps ${ownProps}`);
console.log(ownProps);
return {
customerLoading: state.saleInvoiceReducer.customerLoading,
customerError: state.saleInvoiceReducer.customerError,
productError: state.lineItemsReducer.error,
productLoading: state.lineItemsReducer.loading,
saleInvoiceError: state.saleInvoiceReducer.error,
saleInvoiceLoading: state.lineItemsReducer.error,
saleInvoiceId: state.saleInvoiceReducer.saleInvoiceId,
customerData: state.saleInvoiceReducer.customerData, // data of all customers
productData: state.lineItemsReducer.productData, // data of all products
customerInfo: state.saleInvoiceReducer.customerInfo, // data of current customer
addingCustomer: state.saleInvoiceReducer.addingCustomer, // modal show/hide
customerName: state.saleInvoiceReducer.customerName, // input field name
grandTotal: subTotalSelector(state),
};
};
const mapDispatchToProps = (dispatch, ownProps) => {
return {
fetchCustomer: () => dispatch(fetchCustomer()),
fetchProduct: () => dispatch(fetchProduct()),
getInvoiceById: () =>
dispatch(getInvoiceById(ownProps.location.state.id)),
onBlurCustomerName: event => dispatch(onBlurCustomerName(event)),
stopAddingCustomer: () => dispatch(stopAddingCustomer()),
};
};
const SaleInvoiceContainer = connect(
mapStateToProps,
mapDispatchToProps,
)(SaleInvoice);
SaleInvoice.js (excluding imports)
class SaleInvoice extends React.Component {
state = {
customerName: '',
};
componentWillMount() {
// if api is called from here, state will not update when api updates
// props change cause re-render
this.props.getInvoiceById();
this.props.fetchCustomer();
this.props.fetchProduct();
}
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.customeName !== prevState.customerName) {
return {customerName: nextProps.customerName};
} else return null;
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.customerName !== this.state.customerName) {
let customerName = this.state.customerName;
//Perform some operation here
this.setState({customerName});
}
}
render() {
console.log(this.props);
let ui = this.props.customerError ? (
<p>Customers failed to load!</p>
) : (
<Spinner />
);
let printLink = '/sale-invoice/' + this.props.saleInvoiceId + '/print';
let sui = this.props.productError ? (
<p>Products failed to load!</p>
) : (
<Spinner />
);
if (
!this.props.customerLoading &&
!this.props.customerError &&
!this.props.error
) {
console.log('Customers have been loaded');
ui = (
<React.Fragment>
<Modal
show={this.props.addingCustomer}
modalClosed={this.props.stopAddingCustomer}
customerData={this.props.customerData}
name={this.state.customerName}
/>
<div className={classes.mainContainerTitle}>
{console.log(this.props.grandTotal)}
<h5 className={classes.pageTitle}>Sale Invoice</h5>
<NavLink className={classes.NavLink} to={printLink}>
Print
</NavLink>
{/*<button>Print</button>*/}
</div>
<rs.Container
fluid
className={[classes.mainContainer, classes.containerFluid].join(
'',
)}>
<rs.Row className={classes.firstRow}>
<Customer
customerData={this.props.customerData}
onBlurCustomerName={this.props.onBlurCustomerName}
customerInfo={this.props.customerInfo}
customerName={this.state.customerName}
/>
<SaleInvoiceSummary grandTotal={this.props.grandTotal} />
</rs.Row>
</rs.Container>
</React.Fragment>
);
}
if (
!this.props.productLoading &&
!this.props.productError &&
!this.props.error
) {
console.log('Products have been loaded');
sui = (
<React.Fragment>
<rs.Container fluid className={classes.gridContainer}>
<LineItemsContainer />
</rs.Container>
</React.Fragment>
);
}
return (
<React.Fragment>
{ui}
{sui}
</React.Fragment>
);
}
}
Customer.js (exluding imports)
const Customer = props => {
function _renderMenuItemChildren(option, props, index) {
return [
<Highlighter key="name" search={props.text}>
{option.name}
</Highlighter>,
<div key="place">
<small>Place: {option.place}</small>
</div>,
];
}
return (
<React.Fragment>
{console.log(props.customerName)}
<rs.Card col="sm-4" className={classes.firstCard}>
<rs.CardHeader className={classes.cardHeader}>
Customer Details
</rs.CardHeader>
<rs.CardBody className={classes.cardBodySaleInvoice}>
<rs.Label>Name</rs.Label>
<React.Fragment>
<Typeahead
className={classes.customerTypeahead}
defaultInputValue={props.customerName}
allowNew={true}
newSelectionPrefix="Add New: "
disabled={false}
labelKey="name" // this determines what array key value to show
multiple={false}
options={props.customerData}
placeholder="Choose a customer..."
onBlur={event => props.onBlurCustomerName(event)}
renderMenuItemChildren={_renderMenuItemChildren}
/>
<rs.FormGroup />
</React.Fragment>
<div className={classes.customerCardBody}>
<rs.Label>Address</rs.Label>
<div className={classes.address}>
{props.customerInfo.addressLineOne}
<br />
{props.customerInfo.addressLineTwo}
<br />
{props.customerInfo.address_line_three}
<br />
{props.customerInfo.contact_no_one}
<br />
{props.customerInfo.gst_number}
<br />
<button>Edit</button>
</div>
</div>
</rs.CardBody>
</rs.Card>
</React.Fragment>
);
};
(PS: I am new to React, additional comments/criticism regarding the code will be helpful)
I added a html input component and found that it worked correctly.
The issue was with the Typeahead component.
https://github.com/fmoo/react-typeahead/issues/74#issuecomment-112552406
Also, now using react-select instead of Typeahead and that works correctly too.

Resources