Imagine simple React component connected to Redux, which renders DOM node with value from redux store.
const mapStateToProps = state => ({ text: state.text })
const Text = connect(mapStateToProps)(({ text }) => text)
return default Text
<Text />
Now I want to add modifier property to Text component, which adds one if statement to mapStateToProps.
const mapStateToProps = state => {
const text = state.text
return { text: modifier ? modifier(text) : text }
}
<Text modifier={foo => foo + 2}/>
The question is whether I should have two different components or one with if inside mapStateToProps. My application is heavy loaded data driven app where store could be updated several times per second. 50% of Text components require modifier, the other 50% do not.
Please advice. Thank You.
Related
I'm using the following code to test a state-dependent react component using jest and rtl:
test("render author, date and image correctly after going next post", async () => {
const store = configureStore({
reducer: {
data: dataReducer
}
});
const Wrapper = ({ children }) => (
<Provider store={store}>{children}</Provider>
);
render(<Post />, { wrapper: Wrapper });
const getSpy = jest.spyOn(axios, 'get').mockReturnValue(mockPostJSON);
await store.dispatch(FETCH_POSTS());
expect(getSpy).toBeCalledWith('https://www.reddit.com/r/EarthPorn/.json');
const beforeClick = await screen.findByTestId('authorAndDate');
expect(beforeClick.innerHTML.toString()).toBe(mockPostsList[0].author + ' - ' + mockPostsList[0].date);
fireEvent.click(screen.getByText('Next post'));
const afterClick = await screen.findByTestId('authorAndDate');
expect(afterClick.innerHTML.toString()).toBe(mockPostsList[1].author + ' - ' + mockPostsList[1].date);
})
The problem I'm having is that before the click everything in the store is set up correctly and the authorAndDate element displays the first item in the array of posts. But after the click is fired the store goes back to the initial state it had before loading the mock data. I checked within the component's event handler and right before it does anything the state has been reset. The code is as follows:
const handleNextClick = () => {
store.dispatch(GO_NEXT_POST());
store.dispatch(FETCH_COMMENTS());
}
I've been an hour over the code trying to find something that would reset the state and found nothing. I'm guessing it's some kind of interaction between jest and rtl but I can't figure out why the store in the test has one state and the store in the component's event handler has another :S
Well, figured it out. Can't use store.dispatch directly as it's accessing a stale state. Needed to use the useDispatch hook. Hope this serves anybody who faces the same problem in the future.
in a react UI I have a table component. You can edit one row of the table by clicking a edit button or you can add a new record by clicking a "new-record-button". When clicking the edit button an redux-action is triggered which takes the row and sets a visible property of a modal dialog. When the "new-record-button" is clicked an action is triggered which creates a new empty data item and the same modal dialog is triggered.
In the modal dialog I have several text components with onChange method.
in this onChange-method the data-item is written.
When to user clicks a save-button the edited dataItem is saved to the database.
So my code looks like:
const mapStateToProps = (state) => ({
dataItem: state.datItemToEdit || {},
...
});
...
handleTextChange(event) {
const {
dataItem
} = this.props;
const id = event.target.id;
const text = event.target.value;
switch (id) {
case 'carId': {
dataItem.carId = text;
break;
}
...
}
this.forceUpdate();
}
...
<TextField
...
onChange={event => this.handleTextChange(event)}
/>
I have several question regarding this approach. First I do not understand why in handleTextChange we can write to dataItem. It does work apparently.
dataItem.carId is set in the example code but I thought
const {dataItem} = this.props;
gives us a local read-only variable dataItem just to read from the props...
Next thing I think is a poor design. After reading in a book about react I think we should not write to props but only set a state.
In my example I get the the dataItem from the redux-state. The mapStateToProps maps it to the (read-only) props of the component, right?!. But I want to EDIT it. So I would have to copy it to the state of my component?
But where to do it?
Once in the state of my component I could simply call this.setState for the various text-fields and the component would render and I could abstain from forceUpdate(), right?!
Can someone explain how the redux status plays together with the component status and props for this example?
In redux or react, you shouldn't write to the props directly because you should keep your props as immutable. Redux forces us to use immutable state because state is a source of truth for the application. If the reference to state changes then only your app should render. If you'll mutate your state (objects) then the references don't get changed and your app doesn't know whether some state has been changed or not. React/Redux doesn't give you read-only objects automatically. You can mutate them anytime but as I told you, it can cause problems that Your app won't know when to re-render. If you want to have this read-only property inherently, you should probably use immutable.js
About your second question that you'll have to copy the props to the component's state and where you should do it. You should do it in the constructor of the component and you should use immutibility helper
import React from React;
import update from 'immutibility-helper';
class Modal extends React.Component {
constructor(props){
this.state = {
dataItem: dataItem,
};
}
...other methods
handleTextChange(event) {
const {
dataItem
} = this.props;
const id = event.target.id;
const text = event.target.value;
switch (id) {
case 'carId': {
this.props.updateItem(this.state.dataItem, text); //fire a redux action to update state in redux
this.setState(update(this.state, {
dataItem: {
carId: {$set: text},
}
});
break;
}
...
}
}
}
You wouldn't have to do forceUpdate in such case because the reference to state will change and the component will re-render itself.
Also, you can use forceUpdate in your application but personally I don't find it a great idea because when React/Redux is giving you the flow of state, by using forceUpdate, you're breaking the flow.
The last question is how redux and react state plays together. That is also a matter of choice. If I have a app level state, e.g., in your case you've some app level data, you should put that in your redux state and if you have a component level things, such as opening a modal or opening a third pane. That's the convention I follow but that can really depend on how you want to exploit react and redux state.
Also, in above code, I put the redux state in component state too (because you asked where to put that) but Ideally you should fire a redux action and update in redux state. In this way, you will restrict yourself from state duplication in react and redux.
import React from React;
import {updateItem} from './actions';
class Modal extends React.Component {
...other methods
handleTextChange(event) {
const {
dataItem
} = this.props;
const id = event.target.id;
const text = event.target.value;
switch (id) {
case 'carId': {
this.props.updateItem(this.props.dataItem, text); //fire a redux action to update state in redux
break;
}
...
}
}
}
const mapStateToProps = (state) => ({
dataItem: getDataItem(state), //get Data Item gets Data from redux state
});
export default connect(mapStateToProps, {updateItem: updateItem})(Modal);
in Actions:
updateItem = (dataItem, text) => dispatch => {
dispatch({type: 'UPDATE_ITEM', payLoad: {dataItem, text});
};
in Reducer:
export default (state = {}, action) => {
switch(action){
case 'UPDATE_ITEM': {
return {
...state,
dataItem: {
...action.dataItem,
carId: action.text,
}
};
}
}
}
In this way, your state will be pure and you don't have to worry about immutibility.
EDIT:
As constructor will be called only once, you should probably use componentWillReceiveProps so that whenever you render the component, you get the next updated props of the component. You can check whether the carId of dataItem is same or not and then update the state.
componentWillReceiveProps(nextProps){
if(nextProps.dataItem.carId !== this.props.dataItem.carId){
this.setState({dataItem: nextProps.dataItem});
}
}
You should only use redux when you want different, unrelated components in your app to know and share the specific state.
e.g. - When a user logs in to your app, you might want all components to know that user so you'll connect your different containers to the user reducer and then propagate the user to the components.
Sounds like in this case you have a classic use case for using the inner state.
You can use the parent of all TextFields to maintain all rows, edit them by index, etc.
Once you start using redux, it's really easy to make the mistake of transferring the entire state of the components to the reducers, I've been there and stopped doing it a while ago :)
I have a redux-form component which gets initialValues from the redux store in the first load and populates the form. The form is still not submitted. (it was submitted previously but we are in new state from scratch after a user browser reload). The initial Values are populated properly.
let CheckoutStep1Form = (props) => {
return(
//my fancy form
)
}
CheckoutStep1Form = reduxForm({
form: 'step1', // a unique name for this form
})(CheckoutStep1Form);
CheckoutStep1Form = connect(
state => ({
initialValues: {
//my initial values
}
})
)(CheckoutStep1Form)
Besides this I have another component which is aiming to summarise all the form information provided in previous steps in a plain view.
const CheckoutStep1Summary = ({values}) => {
return(
//my fancy summarize view
)
}
export default connect(
state => ({
values: getFormValues('step1')(state)
})
)(CheckoutStep1Summary)
Problem: getFormValues('step1')(state) returns empty. No values in the form due not submitted.
So the question is: Can I get the form Values from a form that it was not already submitted in some way using redux-form?
Of course I could do some workaround as I have the values in the state but I would like to know if redux-form is taking in consideration this scenario.
I'm confused, are the values in the form or not? You say "The initial Values are populated properly", which leads me to think yes, but getFormValues() isn't doing anything more than returning the values from Redux. I'd recommend using Redux Dev Tools to suss out just what the form state looks like.
You might have just typed it wrong in this question, but this:
CheckoutStep1Form = connect(
state => ({
initialValues: {
//my initial values
}
})
)
should be this:
CheckoutStep1Form = connect(
state => ({
initialValues: {
//my initial values
}
})
)(CheckoutStep1Form) // <-------
Hope that helps...
Let's say I have these components:
Translator
TranslationList
Translator determines translation context, has translate function.
TranslationList must show these "visual states": loading, result list, no results.
The Translator moves around the page (one instance): on focusing an input, it moves "below" it and gives a dropdown with suggestion.
So each time it moves, it has to:
Show that it's loading translations
Show translation list or no results message.
So my question is:
Which component should control the "loading" visual state?
If the Translator component controls it, it has to pass loading=true translations=[] as props to Translation list. Then later it has to rerender it again with new props loading=false translations=[...]. This seems a bit counter-intuitive, because loading feels like the state of the TranslationList component.
If we the TranslationList component has loading state, then it also has to have a way to translate things, meaning that I have to pass translate function as prop. I would then hold translations and loading as state. This all gets a bit messy, since it must now also receive string to translate, context.
I also don't want to have separate components for loading message, no results message. I'd rather keep these inside the TranslationList, because these 3 share that same wrapper <div class="list-group"></div>
Perhaps there should be one more Component in between these two components, responsible only for fetching translation data?
Translator component should control the loading state of a lower component list component. hold the loading and translating logic but with help by wrapping it in a high order component where you should put most of the logic. link for HOC https://www.youtube.com/watch?v=ymJOm5jY1tQ.
const translateSelected = wrappedComponent =>
//return Translator component
class extends React.Component {
state = {translatedText: [], loading:true}
componentDidMount(){
fetch("text to translate")
.then(transText => this.setState({translatedText: transText, loading: false}))
}
render() {
const {translatedText} = this.state
return <WrappedComponent {..this.props} {...translatedText}
}
}
const Translator_HOC = translateSelected(Translator);
You could introduce a Higher Order Component to control the switching of the loading state and the TranslationList. That way you separate the loading display away from your TranslationList as being it's concern. This also allows you to use the HOC in other areas.
The Translator can act as "container" component which does the data fetching/passing.
For example:
// The Loadable HOC
function Loadable(WrappedComponent) {
return function LoadableComponent({ loaded, ...otherProps }) {
return loaded
? <WrappedComponent {...otherProps} />
: <div>Loading...</div>
}
}
// Translation list doesn't need to know about "loaded" prop
function TranslationList({ translations }) {
return (
<ul>
{
translations.map((translation, index) =>
<li key={index}>{translation}</li>
)
}
</ul>
)
}
// We create our new composed component here.
const LoadableTranslationList = Loadable(TranslationList)
class Translator extends React.Component {
state = {
loaded: false,
translations: []
}
componentDidMount() {
// Let's simulate a data fetch, typically you are going to access
// a prop like this.props.textToTranslate and then pass that to
// an API or redux action to fetch the respective translations.
setTimeout(() => {
this.setState({
loaded: true,
translations: [ 'Bonjour', 'Goddag', 'Hola' ]
});
}, 2000);
}
render() {
const { loaded, translations } = this.state;
return (
<div>
<h3>Translations for "{this.props.textToTranslate}"</h3>
<LoadableTranslationList loaded={loaded} translations={translations} />
</div>
)
}
}
ReactDOM.render(<Translate textToTranslate="Hello" />)
Running example here: http://www.webpackbin.com/NyQnWe54W
Working on render performance on React, wonder what is the best way to tackle this performance issue. (The code is overly simplified for clarity)
var TodoList = React.createClass({
getInitialState: function () {
return { todos: Immutable.List(['Buy milk', 'Buy eggs']) };
},
onTodoChange: function (index, newValue) {
this.setState({
todos: this.state.todos.set(index, newValue)
});
},
render: function () {
return (
this.state.todos.map((todo, index) =>
<TodoItem
value={todo}
onChange={this.onTodoChange.bind(null, index)} />
)
);
}
});
Assume only one single todo item has been changed. First, TodoList.render() will be called and start re-render the list again. Since TodoItem.onChange is binding to a event handler thru bind, thus, TodoItem.onChange will have a new value every time TodoList.render() is called. That means, even though we applied React.addons.PureRenderMixin to TodoItem.mixins, not one but all TodoItem will be re-rendered even when their value has not been changed.
I believe there are multiple ways to solve this, I am looking for an elegant way to solve the issue.
When looping through UI components in React, you need to use the key property. This allows for like-for-like comparisons. You will probably have seen the following warning in the console.
Warning: Each child in an array or iterator should have a unique "key" prop.
It's tempting to use the index property as the key, and if the list is static this may be a good choice (if only to get rid of the warning). However if the list is dynamic, you need a better key. In this case, I'd opt for the value of the todo item itself.
render: function () {
return (
this.state.todos.map((todo, index) => (
<TodoItem
key={todo}
value={todo}
onChange={this.onTodoChange.bind(null, index)}
/>
))
);
}
Finally, I think your conjecture about the nature of the onChange property is off the mark. Yes it will be a different property each time it is rendered. But the property itself has no rendering effect, so it doesn't come into play in the virtual DOM comparison.
UPDATE
(This answer has been updated based on the conversation below.)
Whilst it's true that a change to a non-render based prop like onChange won't trigger a re-render, it will trigger a virtual DOM comparison. Depending on the size of your app, this may be expensive or it may be trivial.
Should it be necessary to avoid this comparison, you'll need to implement the component's shouldComponentUpdate method to ignore any changes to non-render based props. e.g.
shouldComponentUpdate(nextProps) {
const ignoreProps = [ 'onChange' ];
const keys = Object.keys(this.props)
.filter((k) => ignoreProps.indexOf(k) === -1);
const keysNext = Object.keys(nextProps)
.filter((k) => ignoreProps.indexOf(k) === -1);
return keysNext.length !== keys.length ||
keysNext.some((k) => nextProps[k] !== this.props[k]);
}
If using state, you'll also need to compare nextState to this.state.