Clearing specific redux state before rendering component - javascript

I have the following "Buy" button for a shopping cart.
I also have a component called Tooltip, which will display itself for error/success messages. It uses the button's width to determine it's centre point. Hence, I use a `ref since I need to access it's physical size within the DOM. I've read that it's bad news to use a ref attribute, but I'm not sure how else to go about doing the positioning of a child component that is based off the physical DOM. But that's another question... ;)
I am persisting the app's state in localStorage. As seen here:
https://egghead.io/lessons/javascript-redux-persisting-the-state-to-the-local-storage
The issue I'm running into is that I have to clear the state's success property before rendering. Otherwise, if I have a success message in the state, on the initial render() the Tooltip will attempt to render as well. This won't be possible since the button it relies on is not yet in the DOM.
I thought that clearing the success state via Redux action in componentWillMount would clear up the success state and therefore clear up the issue, but it appears that the render() method doesn't recognize that the state has been changed and will still show the old value in console.log().
My work-around is to check if the button exists as well as the success message: showSuccessTooltip && this.addBtn
Why does render() not recognize the componentWillMount() state change?
Here is the ProductBuyBtn.js class:
import React, { Component } from 'react';
import { connect } from 'react-redux'
// Components
import Tooltip from './../utils/Tooltip'
// CSS
import './../../css/button.css'
// State
import { addToCart, clearSuccess } from './../../store/actions/cart'
class ProductBuyBtn extends Component {
componentWillMount(){
this.props.clearSuccess()
}
addToCart(){
this.props.addToCart(process.env.REACT_APP_SITE_KEY, this.props.product.id, this.props.quantity)
}
render() {
let showErrorTooltip = this.props.error !== undefined
let showSuccessTooltip = this.props.success !== undefined
console.log(this.props.success)
return (
<div className="btn_container">
<button className="btn buy_btn" ref={(addBtn) => this.addBtn = addBtn } onClick={() => this.addToCart()}>Add</button>
{showErrorTooltip && this.addBtn &&
<Tooltip parent={this.addBtn} type={'dialog--error'} messageObjects={this.props.error} />
}
{showSuccessTooltip && this.addBtn &&
<Tooltip parent={this.addBtn} type={'dialog--success'} messageObjects={{ success: this.props.success }} />
}
</div>
);
}
}
function mapStateToProps(state){
return {
inProcess: state.cart.inProcess,
error: state.cart.error,
success: state.cart.success
}
}
const mapDispatchToProps = (dispatch) => {
return {
addToCart: (siteKey, product_id, quantity) => dispatch(addToCart(siteKey, product_id, quantity)),
clearSuccess: () => dispatch(clearSuccess())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ProductBuyBtn)

Well, it seems to be a known problem that's easy to get into (harder to get out of, especially in a nice / non-hacky way. See this super-long thread).
The problem is that dispatching an action in componentWillMount that (eventually) changes the props going in to a component does not guarantee that the action has taken place before the first render.
So basically the render() doesn't wait for your dispatched action to take effect, it renders once (with the old props), then the action takes effect and changes the props and then the component re-renders with the new props.
So you either have to do what you already do, or use the components internal state to keep track of whether it's the first render or not, something like this comment. There are more suggestions outlined, but I can't list them all.

Related

Why aren't my props available to use in my component when I need them?

I keep running into similar issues like this, so I must not fully understand the lifecycle of React. I've read a lot about it, but still, can't quite figure out the workflow in my own examples.
I am trying to use props in a child component, but when I reference them using this.props.item, I get an issue that the props are undefined. However, if the app loads and then I use the React Browser tools, I can see that my component did in fact get the props.
I've tried using componentDidMount and shouldComponentUpdate in order to receive the props, but I still can't seem to use props. It always just says undefined.
Is there something I'm missing in React that will allow me to better use props/state in child components? Here's my code to better illustrate the issue:
class Dashboard extends Component {
state = { reviews: [] }
componentDidMount () {
let url = 'example.com'
axios.get(url)
.then(res => {
this.setState({reviews: res.data })
})
}
render() {
return(
<div>
<TopReviews reviews={this.state.reviews} />
</div>
);
}
}
export default Dashboard;
And then my TopReviews component:
class TopReviews extends Component {
state = { sortedReviews: []}
componentDidMount = () => {
if (this.props.reviews.length > 0) {
this.sortArr(this.props.reviews)
} else {
return <Loader />
}
}
sortArr = (reviews) => {
let sortedReviews = reviews.sort(function(a,b){return b-a});
this.setState({sortedReviews})
}
render() {
return (
<div>
{console.log(this.state.sortedReviews)}
</div>
);
}
}
export default TopReviews;
I'm wanting my console.log to output the sortedReviews state, but it can never actually setState because props are undefined at that point in my code. However, props are there after everything loads.
Obviously I'm new to React, so any guidance is appreciated. Thanks!
React renders your component multiple times. So you probably see an error when it is rendered first and the props aren't filled yet. Then it re-renders once they are there.
The easy fix for this would be to conditionally render the content, like
<div>
{ this.props.something ? { this.props.something} : null }
</div>
I would also try and avoid tapping into the react lifecycle callbacks. You can always sort before render, like <div>{this.props.something ? sort(this.props.something) : null}</div>
componentDidMount is also very early, try componentDidUpdate. But even there, make your that your props are present.
For reference: see react's component documentation

Need to update state on stateless component using react-redux

I have a component that is connected to a store using react-redux (I have shortened my code for simplicity sake):
const Address = (props) => {
const { userAddresses, removeAddress } = props
let { showEdit } = props
return (
<div>
{userAddresses.map(address => (
<div key={address.id}>
<p>{address.name}</p>
<Button onClick={removeAddress.bind(this, address)}>Delete</Button>
</div>
))}
<Button
onClick={() => { showEdit = true }}>
Add new
</Button>
{showEdit ? (
// show some edit stuff here!!!
): null}
</div>
)
}
const mapState = state => {
return {
userAddresses: state.account.userAddresses,
showEdit: false
}
}
const mapDispatch = (dispatch) => {
return {
addAddress: address => dispatch(addUserAddress(address)),
removeAddress: address => dispatch(removeUserAddress(address)),
}
}
export default connect(mapState, mapDispatch)(Address)
When you click the button (Add new), a form is supposed to popup (marked as show some edit stuff here!!!). I know this can be easily done if Address was a state component. However, I need to use react-redux, and as far as I know, you have to use a stateless component to use react-redux. Can anyone point me in the right direction?
No, you do not "have to use a stateless/function component" to use React-Redux!
connect accepts both class components and function components (and even "special" React components like React.memo()), and it doesn't matter whether they use component state (or hooks) inside or not.
Also, as a side note, you can simplify your code using the "object shorthand" form of mapDispatch:
const mapDispatch = {
addAddress : addUserAddress,
removeAddress : removeUserAddress
}
(Note that that could be even shorter if your prop names were named the same as the functions.)
Keeping to strictly use redux, you should have another action to dispatch when the user clicks the button. Then, a reducer will update the value of the showEdit property, which will cause a re-render of your stateless component allowing you to conditionally render the editing form.
But, this is an information (the visibility or not of the editing form) not useful to the rest of your application, so it could be the case to transform your component into a stateful one and track the showEdit property in the local state.
A third option could be the use of useState hook, but it depends on the version of React you have in your project, because they are currently in alpha...

React-redux : listening to state changes to trigger action

I have my redux state like this:
{
parks: [
{
_id:"ad1esdad",
fullName : "Some Name"
},
{
_id:"ad1es3s",
fullName : "Some Name2"
}
],
parkInfo: {
id : "search Id",
start_time : "Some Time",
end_time : "Some Time"
}
}
I have a parkSelector component from which a user selects parkId and start_time and end_time
import React, { Component } from 'react';
import { changeParkInfo } from '../../Actions';
class ParkSelector extends Component {
constructor(props) {
super(props);
this.handleApply = this.handleApply.bind(this);
this.rederOptions = this.rederOptions.bind(this);
this.state = {
startDate: moment().subtract(1, 'days'),
endDate: moment(),
parkId : this.props.parks[0]
};
}
handleApply(event) {
this.setState({
parkId : event.target.parkId.value
startDate: event.target.start_time.value,
endDate: event.target.end_time.value,
});
this.props.changeParkInfo(this.state.parkId,this.state.startDate,this.state.endDate);
}
rederOptions(){
return _.map(this.props.parks,(park,index)=>{
return(
<option value={park._id} key={park._id}>{park.profile.fullName}</option>
);
});
}
render() {
return (
<div className="row">
<div className="pb-4 col-sm-3">
<form onSubmit={this.handleApply}>
<select name="parkId" value={this.state.parkId} className="form-control input-sm">
{this.rederOptions()}
</select>
<input name="start_time" type="date" />
<input name="end_time" type="date" />
<button type="submit">Apply</button>
</form>
</div>
</div>
)
}
}
function mapStateToProps(state){
return {
parks : state.parks
};
}
export default connect(mapStateToProps,{ changeParkInfo })(ParkSelector);
I have another component 'stats' which needs to displays information related with parkInfo which will be loaded my api request.
import React, { Component } from 'react';
import StatsCard from '../../components/StatsCard';
import { getDashboardStats } from '../../Actions';
class Dashboard extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="animated fadeIn">
<div className="row">
<StatsCard text="Revenue Collected" value={9999} cardStyle="card-success" />
<StatsCard text="Total Checkins" value={39} cardStyle="card-info" />
<StatsCard text="Total Checkouts" value={29} cardStyle="card-danger" />
<StatsCard text="Passes Issued" value={119} cardStyle="card-warning" />
</div>
</div>
)
}
}
function mapStateToProps(state){
return {
parkInfo : state.parkInfo,
dashboardStats : state.dashboardStats
};
}
export default connect(mapStateToProps,{ getDashboardStats })(Dashboard);
I need to call getDashboardStats action (which makes api call and stores in results in dashboardStats of the redux state) whenever the redux state of parkInfo changes.
What is the best way to call this action, I have tried componentWillUpdate but it keeps on updating infinitely. What is best practice for this scenario ?
I had a similar problem but found a suitable approach. I believe the problem has to do with how the responsibilities of reducers and subscribers in a Redux app are often interpreted. My project did not use React, it was Redux-only, but the underlying problem was the same. To put emphasis on answering the underlying problem, I approach the subject from a general perspective without directly referring to your React-specific code.
Problem re-cap: In the app, multiple actions P,Q,R can cause a state change C. You want this state change C to trigger an asynchronous action X regardless of the action that originally caused the state change C. In other words, the async action X is coupled to the state change but intentionally decoupled from the wide range of actions (P,Q,R) that could cause the change C. Such situation does not happen in simple hello-todo examples but does happen in real-world applications.
Naïve answer 1: You cannot trigger another action, it is going to cause infinite loop.
Naïve answer 2: You cannot trigger another action, reducer must not trigger actions or cause any side effects.
Although both naïve answers are true, their base assumptions are wrong. The first wrongly assumes the action X is triggered synchronously and without any stopping condition. The second wrongly assumes the action X is triggered in a reducer.
Answer:
Trigger the action X in a subscriber (aka renderer) and in asynchronous manner. It might sound weird at first but it is not. Even the simplest Redux applications do it. They listen state changes and act based on the change. Let me explain.
Subscribers, in addition to rendering HTML elements, define how actions are triggered in a response to user behaviour. As well as dealing with user behaviour, they can define how actions are triggered in a response to any other change in the world. There is little difference between a user clicking a button after a five seconds and a setTimeout triggering an action after five seconds. As well as we let subscribers to bind an action to a click event or, say, found GPS location, we can let them bind an action to a timeout event. After init, these bindings are allowed to be modified at each state change just like how we can re-render a button or the whole page at a state change.
An action triggered by setTimeout will cause a loop-like structure. Timeout triggers an action, reducers update the state, redux calls subscribers, subscribers set a new timeout, timeout triggers action etc. But again, there is little difference to the loop-like structure caused by normal rendering and binding of events with user behaviour. They are both asynchronous and intended cyclic processes that allow the app to communicate with the world and behave as we like.
Therefore, detect your state change of interest in a subscriber and freely trigger the action if the change happened. Use of setTimeout can be recommended, even with the delay of zero, just to keep the event loop execution order clear to you.
A busy loop is of course a problem and must be avoided. If the action X itself causes such state change that will immediately trigger it again, then we have a busy loop and the app will stop to respond or become sluggish. Thus, make sure the triggered action does not cause such a state change.
If you like to implement a repeating refresh mechanism, for example to update a timer each second, it could be done in same manner. However, such simple repetition does not need to listen state changes. Therefore in those cases it is better to use redux-thunk or write an asynchronous action creator otherwise. That way the intention of the code becomes easier to understand.
If my understanding is correct, you need to make the API call to get the dashboardStats in your Dashboard component, whenever the parkInfo changes.
The correct life-cycle hook in this scenario would be the componentWillReceiveProps
componentWillReceiveProps(nextProps){
// this check makes sure that the getDashboardStats action is not getting called for other prop changes
if(this.props.parkInfo !== nextProps.parkInfo){
this.props.getDashboardStats()
}
}
Also note that, componentWillReceiveProps will not be called for the first time, so you may have to call the this.props.getDashboardStats() in componentDidMount too.
Goal: A change in parkInfo redux-state should prompt Dashboard to dispatch getDashboardInfo and re-render. (This behavior will also be similar in other components).
I use babel transform-class-properties, syntax is slightly different.
example:
// SomeLayout.js
import ParkSelector from 'containers/ParkSelector'
import Dashboard from 'containers/Dashboard'
const SomeLayout = () => {
return (
<div>
<ParkSelector />
<Dashboard />
</div>
)
}
export default SomeLayout
-
// Dashboard.js
// connect maps redux-state to *props* not state, so a new park selection
// will not trigger this component to re-render, so no infinite loop there
#connect((store) => ({ currentParkId: store.parkInfo.id }, //decorator syntax
{ getDashboardStats })
)
class Dashboard extends Component {
state = {
currentId: this.props.currentParkID,
parkInfoFoo: '',
parkInfoBar: ''
}
// using null for when no park has been selected, in which case nothing runs
// here.
componentWillReceiveProps(nextProps) {
// when Dashboard receives new id via props make API call
// assumes you are setting initial state of id to null in your reducer
if (nextProps.currentParkId !== null) {
getDashboardStats(`someurl/info/${nextProps.id}`).then((data) => {
// update state of Dashboard, triggering a re-render
this.setState({
currentId: nextProps.id
parkInfoFoo: data.foo,
parkInfoBar: data.bar
})
})
}
}
render() {
const { currentId, parkInfoFoo } = this.state
if (currentId !== null) {
return <span>{parkInfoFoo}</span>
}
return null
}
}
export default Dashboard
I think it should be this way:
Your component ParkSelector changes, you trigger the action via dispatch to changeParkInfo action.
This action does the AJAX and on it's success
1) you update the store state of parkInfo via another action.
2) you send getDashboardStats.
Now when point (2) is success, it will update the store state dashboardStats.
Next in your Dashboard you should not connect with parkInfo, reason: you are not using parkInfo in the dashboard.
Inside the dashboard component you should call action getDashboardStats in componentDidMount() for loading up the dashboardStats for first time when component loads up.
The idea in nutshell is when data changes action should call the facade of actions and make the state change.
What you trying is trigger an action which changes state that goes into component via props that triggers another action so on.. thus there is a infinite loop as you have written the component action in componentWillUpdate.
Hope this clarifies your question.

How to allow child component to react to a routing event before the parent component?

I am using react, react-router & redux. The structure of my app is such:
CoreLayout
-> <MaterialToolbar /> (contains back button)
-> {children} (react-router)
When the user presses the back button, which is normally handled by the CoreLayout, I would like the current child component to handle the back button instead of the parent. (In my case, I would like the current view to check if its data has been modified, and pop up an 'Are you sure you wish to cancel?' box before actually going back.) If the child does not wish to handle this, the parent will do it's thing.
Another example would be allowing a childview to set the title in the toolbar.
My reading has told me that accessing a component through a ref and calling a method on it is not the react way -- this is also made a bit more difficult since I am using redux-connect. What is the correct way to implement this behavior?
This is how I would do it, assuming you mean your navigation back button (and not the browser back button):
class CoreLayout extends Component {
handleBack () {
//... use router to go back
}
render () {
return <div>
<MaterialToolbar />
{React.children.map(this.props.children, child => React.cloneElement(child, { onBack: this.handleBack }))}
</div>
}
}
class Child extends Component {
handleBackButtonClick () {
// Here perform the logic to decide what to do
if (dataHasBeenModifiedAndConfirmed) {
// Yes, user wants to go back, call function passed by the parent
this.props.onBack()
} else {
// User didn't confirm, decide what to do
}
}
render () {
return <div onClick={this.handleBackButtonClick.bind(this)}>
Go Back
</div>
}
}
You simply pass a function from the parent to the child via props. Then in the child you can implement the logic to check if you really want to delegate the work to the parent component.
Since you use react-router and your children are passed to your parent component through this.props.children, to pass the onBack function you need to map the children and use React.cloneElement to pass your props (see this answer if you need more details on that: React.cloneElement: pass new children or copy props.children?).
Edit:
Since it seems you want to let the children decide, you can do it this way (using refs):
class CoreLayout extends Component {
constructor () {
super()
this.childRefs = {};
}
handleBack () {
for (let refKey in Object.keys(this.childRefs) {
const refCmp = this.childRefs[refKey];
// You can also pass extra args to refCmp.shouldGoBack if you need to
if (typeof refCmp.shouldGoBack === 'function' && !refCmp.shouldGoBack()) {
return false;
}
}
// No child requested to handle the back button, continue here...
}
render () {
return <div>
<MaterialToolbar />
{React.children.map(this.props.children, (child, n) => React.cloneElement(child, {
ref: cmp => { this.childRefs[n] = cmp; }
}))}
</div>
}
}
class Child extends Component {
shouldGoBack () {
// Return true/false if you do/don't want to actually go back
return true
}
render () {
return <div>
Some content here
</div>
}
}
This is a bit more convoluted as normally with React it's easier/more idiomatic to have a "smart" parent that decides based on the state, but given your specific case (back button in the parent and the logic in the children) and without reimplementing a few other things, I think using refs this way is fine.
Alternatively (with Redux) as the other answer suggested, you would need to set something in the Redux state from the children that you can use in the parent to decide what to do.
Hope it's helpful.
I don't think there is a correct way to solve this problem, but there are many ways. If I understand your problem correctly, most of the time the back button onClick handler will be handled within CoreLayout, but when a particular child is rendered that child will handle the onClick event. This is an interesting problem, because the ability to change the functionality of the back button needs to be globally available, or at very least available in CoreLayout and the particular child component.
I have not used redux, but I have used Fluxible and am familar with the Flux architecture and the pub/sub pattern.
Perhaps you can utilize your redux store to determine the functionality of your back button. And your CoreLayout component would handle rendering the prompt. There is a bug with the following code, but I thought I would not delete my answer for the sake of giving you an idea of what I am talking about and hopefully the following code does that. You would need to think through the logic to get this working correctly, but the idea is there. Use the store to determine what the back button will do.
//Core Layout
componentDidMount() {
store.subscribe(() => {
const state = store.getState();
// backFunction is a string correlating to name of function in Core Layout Component
if(state.backFunction) {
// lets assume backFunction is 'showModal'. Execute this.showModal()
// and let it handle the rest.
this[state.backFunction]();
// set function to false so its not called everytime the store updates.
store.dispatch({ type: 'UPDATE_BACK_FUNCTION', data: false})
}
})
}
showModal() {
// update state, show modal in Core Layout
if(userWantsToGoBack) {
this.onBack();
// update store backFunction to be the default onBack
store.dispatch({ type: 'UPDATE_BACK_FUNCTION', data: 'onBack'})
// if they don't want to go back, hide the modal
} else {
// hide modal
}
}
onBack() {
// handle going back when modal doesn't need to be shown
}
The next step is to update your store when the child component mounts
// Child component
componentDidMount(){
// update backFunction so when back button is clicked the appropriate function will be called from CoreLayout
store.dispatch({ type: 'UPDATE_BACK_FUNCTION', data: 'showModal'});
}
This way you don't need to worry about passing any function to your child component you let the state of the store determine which function CoreLayout will call.

FilterLink does not require a subscription to update

In Dan Abramov's egghead.io Redux course, Lecture 22, the video says that the FilterLink component needs to subscribe to the store explicitly (via a forceUpdate) in order for changes to be reflected in the component. Namely, after the SET_VISIBILITY_FILTER type action is dispatched upon clicking on a filter, the current filter (state.visibilityFilter) will change to the one clicked on.
My understanding from the lecture was that if we did not subscribe and do a forceUpdate, the formatting on the filters would not change because the information was not propagated to FilterLink form the store, and then on down to Link.
However, when I removed the lines with componentDidMount and componentWillUnmount in FilterLink component, the app worked fine and it seems the information was still being propagated even without explicitly forcing update from the store.
class FilterLink extends Component {
componentDidMount() {
this.unsubscribe = store.subscribe(() =>
this.forceUpdate()
);
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
const {
filter,
children,
} = this.props;
const state = store.getState();
return (
<Link
active = {filter === state.visibilityFilter}
onClick = {() => store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: filter,
})}
> {children}</Link>
)
}
}
From the code below, we see that only the active link has a <span> (i.e. not underlined) and the non-active filters should appear with an underline below them.
const Link = ({
active,
children,
onClick,
}) => {
if (active) {
return (
<span>
{children}
</span>
)
}
else return (
<a href='#' onClick = { e => {
e.preventDefault();
onClick()
}
}
>{children}</a>
)
}
My question is: The outcomes in the UI whether including or excluding the componentDidMount/componentWillUnmount lines were identical viz. the clicked-on filter will become span and not be underlined and the other two will become <a> and be underlined. This shows that even without an explicit subscription, the information from the store (state.visibilityFilter in this case) has been successfully passed down to <Link> component.
So does the subscription to the store and the forceUpdate in the FilterLink component achieve some sort of update that's important behind the scenes and is not apparent in the UI, or is this step purely optional? If there was an update not apparent in the UI, what was it?
The reason it still renders is because there was still a top level render store.subscribe(render); at this point in the code. Later in the video Dan will remove this top level render and have the classes handle lifecycle solely instead. The code change is here.

Categories

Resources