I have a component which isn't re-rendering as I'd expect. I'm less concerned about this specific example than having a better understanding about how state, props, updates, re-rendering and more happens in the react-redux lifecycle.
My current code is about creating a delivery with a list of locations. The main issue is that reordering the location's itinerary doesn't seem to work - the state updates in the reducer correctly, but the components are not rerendering.
This is the relevant snippet from delivery.js, a component which uses the LocationSearch custom component to display each location in the list of locations:
{console.log("Rendering...")}
{console.log(delivery.locations)}
{delivery.locations.map((location, index) => (
<div key={index}>
<LocationSearch
{...location}
total={delivery.locations.length+1}
index={index}
/>
</div>
))}
The console.logs print out the correct data where and when expected. When an action to reorder the locations is triggered (from within LocationSearch), the console log prints out the list of locations with the data updated correctly. However the component is not displaying anything updated.
Here is some relevant parts of the LocationSearch component:
export class LocationSearch extends Component {
constructor(props) {
super(props)
this.state = {
searchText: this.props.address
}
this.handleUpdateInput = this.handleUpdateInput.bind(this)
}
handleUpdateInput (searchText) {
this.setState({
searchText: searchText
})
this.props.handleUpdateInput(searchText)
}
render(){
const { type, itineraryOrder, floors, elevator, accessDistance, index} = this.props
return (
...display all the stuff
)
}
}
...map dispatch, connect, etc...
function mapStateToProps(state, ownProps) {
return {
suggestions: state.delivery.suggestions,
data: ownProps.data
};
}
This is where I get confused - I figure I'm meant to do something along the lines of componentWillUpdate, but can't find any good explanations for what happens at each step. Do I just set this.props = nextProps in there? Shouldn't this.props have updated already from the parent component passing them in? How come most components seem to rerender by themselves?
Any help you can give or links to good resources I'd be grateful for. Thanks in advance!
I kept running into this kind of issues right before I discovered redux. Considering you mentioned already using react-redux, maybe what you should do is switch to a container/component structure and forget about componentWillUpdate()
Basically, what this allows for is just passing fresh props to the components that renders the actual HTML, so you don't have to replace the props by hand.
Your container could be something like this
import React from 'react'
import { connect } from 'react-redux'
import PresentationalComponent from 'components/PresentationalComponent'
const Container = props => <PresentationalComponent {...props} />
export default connect( state => ({
searchText: state.UI.searchText,
locations: [...],
}), dispatch => ({
handleUpdateInput: e => dispatch( {
type: "CHANGE_SEARCH_TEXT",
text: e.target.value,
} ),
}))(Container)
And your presentational component
import React from 'react'
const PresentationalComponent = ({ searchText, locations, handleUpdateInput }) =>
<div>
{locations.map(location => <p>{location}</p>)}
<input defaultValue={searchText} type="text" onChange={handleUpdateInput} />
</div>
export default PresentationalComponent
Related
I've been fiddling around with MobX and am struggling to understand why the ChildComponent in the following code snippet doesn't react to state updates.
My understanding is that the ChildComponent should update since isOpen.get() changes after every call to controller.updateState() (which is brokered through the button click). I added some log statements and found that the state does update correctly, but the text inside the ChildComponent doesn't and I haven't been able to figure out why. I would really appreciate any help in understanding what I've done wrong here.
Note: I am aware that a call to makeObservable is required as of MobX v6. However, the codebase that I'm working on uses that older version that doesn't support that function call.
App.tsx:
import { computed } from "mobx";
import { observer } from "mobx-react";
import { Controller } from "./controller";
const ChildComponent = observer(({ isOpen }: { isOpen: boolean }) => (
<p>isOpen: {isOpen.toString()}</p>
));
export default function App() {
const controller = new Controller();
const isOpen = computed(() => controller.state === "open");
const onClick = () => {
if (isOpen.get()) {
controller.updateState("closed");
} else {
controller.updateState("open");
}
};
return (
<div className="App">
<ChildComponent isOpen={isOpen.get()} />
<button onClick={onClick}>Toggle state</button>
</div>
);
}
controller.ts:
import { action, observable } from "mobx";
type State = "open" | "closed";
export class Controller {
#observable.ref
state: State;
constructor() {
this.state = "open";
}
#action
updateState = (newState: State) => {
this.state = newState;
};
}
Link to codesandbox: https://codesandbox.io/s/broken-pine-3kwpt?file=/src/App.tsx
Lots of things goes wrong there:
Use makeObservable or downgrade example to MobX version 5, otherwise it won't work
You don't need to use #observable.ref for boolean, just use plain regular #observable
You can't use computed like that. computed is a decorator that should be used inside your store, similar to observable, like that:
#computed
get isOpen() {
return this.state === 'open';
}
In your example App should be wrapped in observer because it dereferences an observable value (isOpen). And every component that does it should be wrapped. At the same time ChildComponent gets isOpen prop as primitive value so it does not benefit from being observer (because it does not reference any observable property).
You need to create your controller differently. Right now you recreate it on every render and even if you fix all the problems above it won't work because every time you change some value the App will rerender and recreate the store with default values.
Hope it makes sense!
Working Codesandbox example with everything fixed
I seem to be having a reoccurring issue that I'm hoping there is a design solution out there that I am not aware of.
I'm running into the situation where I need to dispatch the exact same things from two different components. Normally I would set this to a single function and call the function in both of the components. The problem being that if I put this function (that requires props.dispatch) in some other file, that file won't have access to props.dispatch.
ex.
class FeedScreen extends Component {
.
.
.
componentWillReceiveProps(nextProps) {
let {appbase, navigation, auth, dispatch} = this.props
//This is to refresh the app if it has been inactive for more
// than the predefined amount of time
if(nextProps.appbase.refreshState !== appbase.refreshState) {
const navigateAction = NavigationActions.navigate({
routeName: 'Loading',
});
navigation.dispatch(navigateAction);
}
.
.
.
}
const mapStateToProps = (state) => ({
info: state.info,
auth: state.auth,
appbase: state.appbase
})
export default connect(mapStateToProps)(FeedScreen)
class AboutScreen extends Component {
componentWillReceiveProps(nextProps) {
const {appbase, navigation} = this.props
//This is to refresh the app if it has been inactive for more
// than the predefined amount of time
if(nextProps.appbase.refreshState !== appbase.refreshState) {
const navigateAction = NavigationActions.navigate({
routeName: 'Loading',
});
navigation.dispatch(navigateAction);
}
}
}
const mapStateToProps = (state) => ({
info: state.info,
auth: state.auth,
appbase: state.appbase
})
export default connect(mapStateToProps)(AboutScreen)
See the similar "const navigateAction" blocks of code? what is the best way to pull that logic out of the component and put it in one centralized place.
p.s. this is just one example of this kind of duplication, there are other situations that similar to this.
I think the most natural way to remove duplication here (with a react pattern) is to use or Higher Order Component, or HOC. A HOC is a function which takes a react component as a parameter and returns a new react component, wrapping the original component with some additional logic.
For your case it would look something like:
const loadingAwareHOC = WrappedComponent => class extends Component {
componentWillReceiveProps() {
// your logic
}
render() {
return <WrappedComponent {...this.props} />;
}
}
const LoadingAwareAboutScreen = loadingAwareHOC(AboutScreen);
Full article explaining in much more detail:
https://medium.com/#bosung90/use-higher-order-component-in-react-native-df44e634e860
Your HOCs will become the connected components in this case, and pass down the props from the redux state into the wrapped component.
btw: componentWillReceiveProps is deprecated. The docs tell you how to remedy.
I have just started to explore the Container Component pattern. What I don't quite grasp yet is the concept of presentational component only being concerned with the visuals.
Does that mean that these component can't dispatch action that would change the redux state?
e.g
<MainContainer>
<ListComponent />
<GraphComponent />
</MainContainer>
Say <GraphComponent> shows graphs based on a list in redux state. The <ListComponent> then modify this list in the redux state with buttons. Is this okay in the Container Component pattern?
I think that you're not supposed to dispatch actions in Components. In Container-Component pattern, you're supposed to pass a callback function from the container (MainContainer in your case) as props to ListComponent, that fires when the button is clicked and dispatch action (in container) as result.
Presentation (Dumb) components are like 1st grade students, they have their unique appearance but their behavior and the content they put out is decided or taught by their parents.
Example: <Button />
export const Button = props => {
<button type="button" onClick={props.onClick} />{props.text}</button>
}
Unique appearance: it's a button
Behavior: onClick
Content: text
Both onClick and text are provided by parent.
When they grow to 5th or 7th grade, they may start to have their own state and decide few of the things on their own.
Example: <Input />
class Input extends Component {
constructor(props) {
super(props);
this.state = {
value: ''
}
}
handleChange = (e) => {
this.setState({value: e.target.value});
}
render() {
return (
<input
type="text"
value={this.state.value}
onChange={this.handleChange}
onFocus={this.props.onFocus}
/>
);
}
}
Unique appearance: it's an input
State: decides it own value.
Behavior: onFocus
onFocus provided by parent.
And when they become adults they may start to behave on their own and may not
need their parents guidance. They start to talk to the outer world (redux store)
on their own (now they are new Container components).
Example
const mapStateToProps = (state, [ownProps]) => {
...
}
const mapDispatchToProps = (state, [ownProps]) => {
...
}
const MyComponent = (props) => {
return (
<ChildComponent {...props} />
);
}
connect(mapStateToProps, mapDispatchToProps)(MyComponent);
Decides its own behavior and content, may or may not need parent (read ownProps)
Presentational components may or may not need any behavior of their own,
they rely on their parents for that. But to talk (dispatch) to the outer
world(store) they need to be big enough (Container components).
So the main thing to remember here is start small and decide as your component
grows whether it needs to be a presentational or container component.
I am trying to setState() to a query result I have from graphQL, but I am having difficulty finding out how to do this because it will always be loading, or it's only used from props.
I first set the state
constructor (props) {
super(props);
this.state = { data: [] };
Then I have this query
const AllParams = gql`
query AllParamsQuery {
params {
id,
param,
input
}
}`
And when it comes back I can access it with this.props.AllParamsQuery.params
How and when should I this.setState({ data: this.props.AllParamsQuery.params }) without it returning {data: undefined}?
I haven't found a way to make it wait while it's undefined AKA loading: true then setState. I've tried componentDidMount() and componentWillReceiveProps() including a async function(){...await...} but was unsuccessful, I am likely doing it wrong. Any one know how to do this correctly or have an example?
EDIT + Answer: you should not setstate and just leave it in props. Check out this link: "Why setting props as state in react.js is blasphemy" http://johnnyji.me/react/2015/06/26/why-setting-props-as-state-in-react-is-blasphemy.html
There is more to the problem to update props, but some great examples can be found at this app creation tutorial: https://www.howtographql.com/react-apollo/8-subscriptions/
A simple solution is to separate your Apollo query components and React stateful components. Coming from Redux, it's not unusual to transform incoming props for local component state using mapStateToProps and componentWillReceiveProps.
However, this pattern gets messy with Apollo's <Query />.
So simply create a separate component which fetches data:
...
export class WidgetsContainer extends Component {
render (
<Query query={GET_WIDGETS}>
{({ loading, error, data }) => {
if (loading) return <Loader active inline="centered" />;
const { widgets } = data;
return (
<Widgets widgets={widgets} />
)
}}
</Query>
)
}
And now the Widgets components can now use setState as normal:
...
export class Widgets extends Component {
...
constructor(props) {
super()
const { widgets } = props;
this.state = {
filteredWidgets: widgets
};
}
filterWidget = e => {
// some filtering logic
this.setState({ filteredWidgets });
}
render() {
const { filteredWidgets } = this.state;
return (
<div>
<input type="text" onChange={this.filterWidgets} />
{filteredWidgets.count}
</div>
)
}
}
What is the reason behind setting it to state? Keep in mind, Apollo Client uses an internal redux store to manage queries. If you're trying to trigger a re render based on when something changes in the query, you should be using refetchQueries(). If you absolutely need to store it in local state, I would assume you could probably compare nextProps in componentWillReceiveProps to detect when loading (the value that comes back when you execute a query from apollo client) has changed, then update your state.
I had a similar issue (although it was happening for a totally different reason). My state kept getting set to undefined. I was able to solve it with a React middleware. It made it easy to avoid this issue. I ended up using superagent.
http://www.sohamkamani.com/blog/2016/06/05/redux-apis/
I've implemented Redux in my React application, and so far this is working great, but I have a little question.
I have an option in my navbar to change the locale, stored in redux's state. When I change it, I expect every component to rerender to change traductions. To do this, I have to specify
locale: state.locale
in the mapStateToProps function... Which leads to a lot of code duplication.
Is there a way to implicitly pass locale into the props of every component connected with react-redux ?
Thanks in advance!
Redux implements a shouldComponentUpdate that prevents a component from updating unless it's props are changed.
In your case you could ignore this check by passing pure=false to connect:
connect(select, undefined, undefined, { pure: false })(NavBar);
For performance reasons this is a good thing and probably isn't what you want.
Instead I would suggest writing a custom connect function that will ensure locale is always added to your component props:
const localeConnect = (select, ...connectArgs) => {
return connect((state, ownProps) => {
return {
...select(state, ownProps),
locale: state.locale
};
}, ...connectArgs);
};
// Simply use `localeConnect` where you would normally use `connect`
const select = (state) => ({ someState: state.someState });
localeConnect(select)(NavBar); // props = { someState, locale }
To cut down the duplication of code I usually just pass an arrow function to the connect method when mapping state to props, looks cleaner to me. Unfortunately though, I don't think there is another way to make it implicit as your component could subscribe to multiple store "objects".
export default connect((state) => ({
local: state.locale
}))(component);
To solve this problem, you can set the Context of your parent component, and use it in your child components. This is what Redux uses to supply the store's state and dispatch function to connected React components.
In your Parent component, implement getChildContext and specify each variable's PropType.
class Parent extends React.Component {
getChildContext() {
return {
test: 'foo'
};
}
render() {
return (
<div>
<Child />
<Child />
</div>
);
}
}
Parent.childContextTypes = {
test: React.PropTypes.string
};
In your Child component, use this.context.test and specify its PropType.
class Child extends React.Component {
render() {
return (
<div>
<span>Child - Context: {this.context.test}</span>
</div>
);
}
}
Child.contextTypes = {
test: React.PropTypes.string
};
Here's a demo of it working.
I might as well mention that, while libraries like Redux use this, React's documentation states that this is an advanced and experimental feature, and may be changed/removed in future releases. I personally would not recommend this approach instead of simply passing the information you need in mapStateToProps, like you originally mentioned.