When I use flux-like like Vuex or Redux. I have a question about list state update.
I have list data in flux state.
// state.js
{
itemById: {},
}
And I have item's id array in list component.
// listComponent.js
{
data: {
itemIds: [],
items: data.itemIds.map(id => state.itemById[id]),
};
}
Now I add an item in other component.
// otherComponent.js
addItem(newItem) {
store.dispatch(newItem);
}
Then the itemById state have the newItem, but the itemIds don't have the newItem's id.
if move the itemIds to state. When the listComponent destroy and recreat, itemIds still exist, but I don't want this.
What should I do to update itemIds simply?
Complementing #blaz answer, If you are using redux, in your listComponent.js you must use the connect function from react-redux lib. It will give the possibility to load the data from your state and inject it as props in your component.
const mapStateToProps = (state) => {
const itemIds = Object.keys(state.itemById);
const items = Object.values(state.itemById);
return { itemIds, items };
};
export default connect(mapStateToProps)(ListComponent);
One problem with your code is that you have 2 copies of the same data: one in state and one in data of listComponent. Assuming listComponent's data is initiated from state's data, for every update in state, you need to catch and update local component's data to match them. Destroying and recreating the component does not directly update local data, and it's not recommended to recreate the whole component just for the local data, since it beats the purpose of component life cycle.
Flux aside, one of the reasons people use state management tool like redux or vuex is because of single source of truth - there should be one copy of global state from which any component can read. To fix your code, one solution is to move itemIds and items from data to computed and directly take them from state.
computed: {
itemIds() {
return Object.keys(state.itemByIds);
},
items() {
return Object.values(state.itemByIds);
}
}
Related
I know my question comes from a misunderstanding of react-redux but I will describe my use case hoping someone will point me in the right direction.
I'm trying to store selected row keys (selectedRowKeys) from a table (ant design table) inside of a redux store. Everything works when the store structure is simple like this:
selectedRowKeys: []
But I want to store that state in a normalized form to handle multiple tables and multiple table properities:
tables: {
123fdfas-234dasf-234asdf-23rfa : { //table id
id: 123fdfas-234dasf-234asdf-23rfa,
selectedRowKesy: []
//... other properities
}
}
The problem is that this state doesn't exist when redux is trying to mapStateToProps like this:
const mapStateToProps = (state, ownProps) => {
if (!ownProps.id) {
ownProps.id = uuidv4();
}
return {
selectedRowKeys: state.tables[ownProps.id].selectRowKeys
};
};
state.tables[ownProps.id] is undefined so there is an error.
I thought that I need to initialize the state somehow but this led me to even more confusion. What I have figured out so far:
I can't initialize state in reducer like reducerName (state = initialState, action) because action is not dispatched and there is no action.id (action object has a payload with table id).
I can't dispatch an action INIT_TABLE in componentDidMount() because mapStateToProps executes first so state.tables[ownProps.id] is still undefined.
I feel like this use case is wierd and that is way I cannot find the solution although I have been googling and thinking about this for 3 days.
Please guide me, I'm in a crazy loop :)
This is my first SO question, pls let me know if something is unclear.
Who is responsible for creating a new table? That's the deciding factor on how to solve this problem.
The id definitely shouldnt be created in mapStateToProps and you shouldn't mutate ownProps. I'm surprised if that even works. Id should be created in action if using redux.
If your React-app has some mechanism which creates a new table (for example, user clicks button), then that's where you should dispatch initialization action. If you really can't find parent component which would be responsible for table initialization, then maybe it is responsibility of this component and you should dispatch the action in componentDidMount.
Regardless of which option you pick, your mapStateToProps should handle empty state gracefully ie. selectedRowKeys should be set to some default value if it's missing (empty array or null maybe?). And your component should handle missing values if there is no sane default available. It's common to have some null checks in render-function which return null until data is available.
From my understanding to the problem, you are being too specific!
In this case, it's not a good practice.
I advice the follow.
Supposing that you have the reducer tables:
const mapStateToProps = ({ tables }, ownProps) => {
return {
tables,
};
};
This will maket the reducers tables available at your component's this.props
Thus, you can do the follow wherever you want in the component to get your selected rows, define this method and use it in a proper place instead of directly doing it in mapStateToProps, which is not a good practice.
getSelectedRowKeys = (ownProps) => {
if (!ownProps.id) {
ownProps.id = uuidv4();
}
const selectedRowKeys = [];
const table = this.props.tables[ownProps.id];
const selectedRowKeys = table && table.selectRowKeys; //if table exists get the row keys.
return selectedRowKeys || []; //if selectedRowKeys are not there return an empty array.
}
So I have been trying to use apollo-boost in a React app to use the cache to manage my client state using #client directives on queries but I have been having some issues.
Basically I'm using writeQuery() to write a boolean to my local app state in a Component (let's call it component A) and want to get that value in another Component (let's call it component B) using readQuery() inside the componentDidUpdate method. The thing is, readQuery() in Component B is running before writeQuery in Component A sets the value in the cache/local state so the value read by Component B comes out wrong.
I've confirmed this by using setTimeout to delay the readQuery() and indeed after using the timeout, the value is correct, but this solution can't be trusted, I'm probably not aware of something in Apollo Client because this functionality is pretty basic for local state management. Any Tips?
I believe that in Redux this is solved because the state is being injected to props, which makes the component update, so being that Component A is the one that changes the state, component B wouldn't even have to use componentDidUpdate to get the new value, since the state would be injected and Component B would get updated with the correct value.
Any help would be appreciated, sorry if I didn't make myself clear!
EDIT: The writeQuery() is being used inside a Mutation resolver.
Methods like readQuery and writeQuery are meant to be used to read and modify the cache inside of mutations. In general, they should not be used inside of components directly. By calling readQuery, you are only fetching the data from the cache once. Instead, you should utilize a Query component.
const TODO_QUERY = gql`
query GetTodos {
todos #client {
id
text
completed
}
}
`
<Query query={TODO_QUERY}>
{({ data }) => {
if (data.todos) return <ToDoListComponent todos={data.todos}/>
return null
}}
</Query>
The Query component subscribes to relevant changes to the cache, so the value of data will update when your cache does.
Similarly, you should create appropriate mutations for whatever changes to the cache you're going to make, and then utilize a Mutation component to actually mutate the cache.
const client = new ApolloClient({
clientState: {
defaults: {
todos: []
},
resolvers: {
Mutation: {
addTodo: (_, { text }, { cache }) => {
const previous = cache.readQuery({ query: TODO_QUERY })
const newTodo = { id: nextTodoId++, text, completed: false, __typename: 'TodoItem' }
const data = {
todos: previous.todos.concat([newTodo]),
}
cache.writeQuery({ query, data })
return newTodo
},
},
}
}
})
<Mutation mutation={ADD_TODO}>
{(addTodo) => (
// use addTodo to mutate the cache asynchronously
)}
</Mutation>
Please review the docs for more details.
I want to create an app with react and redux. My component subscribed to several states from the redux store, some of the state-data need to be prepared before the rendering can take place. Do I need to put the prepareData function into componentWillReceiveProps and write it to the state afterwards? It seems to create a lot of queries in the componentWillReceiveProps. Is there a best practice?
componentWillReceiveProps(nextProps) {
if (this.props.dataUser !== nextProps.dataUser) {
this.prepareData(nextProps.dataUser);
}
if (this.props.dataProject !== nextProps.dataProject) {
.....
}
if (this.props.dataTasks !== nextProps.dataTasks) {
.....
}
}
As Axnyff suggests, you can do your data preparation in mapStateToProps, this will trigger a render each time your redux state updates (your component can be stateless this way) :
mapStateToProps = (state) => {
const dataUserPrepared = prepareData(state.dataUser);
return { dataUser: dataUserPrepared };
}
If you have a lot of different data to prepare, which updates individually, that can be a loss in performance.
In this case you can use componentWillReceiveProps like in your question, this is fine because the setState in your prepareData() function will be batched with the received props to trigger only one render per prop update.
If you were using an app without redux then the solution would be to prepare your data before you call this.setState().
I believe the same solution applies to when using redux, your can prepare your data inside your action because you return the action object having a type and payload.
You can also prepare your data inside your reducer before returning the state object.
You could even prepare your data inside mapStateToProps of your component.
But in case you want to specific conditions under which component should re-render when state changes, then you do that in shouldComponentUpdate()
I am new to react-redux and I was surprised to see an example where a function, in this case being getVisiblieTodos, is called inside mapStateToProps. This function should be called in a reducer since it changes state? Is the code breaking "good form" for the sake of brevity? Is it okay to do this in general?
I am looking at code from this link
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch(toggleTodo(id))
}
}
}
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
In redux we want the store to hold the minimal data needed for the app. Everything that is derived from the base data, should be computed on the fly, to prevent cloning pieces of the store, and the need to recompute all derived data when something changes in the store.
Since the visible todos list is not part of the store, but computed using the list of todos, and the visibilityFilter, the getVisibleTodos() doesn't change the store's state. It produces the derived computed data from the those two properties.
A function that is used to get data from the store, and compute derived data is known as a selector. Using selectors, the derived data is not part of the store, and computed when needed. In addition, we can use memoized selectors, to save the computation overhead.
You may see getVisibleTodos as a reducer because it includes "switch .. case" block or/and because it has 2 arguments . However, it is not a rule.
A redux reducer ( by definition) changes store state according to dispatched action , and that's why it takes two arguments ( store state + dispatched action ) and it returns new state for the store without mutation.
getVisibleTodos here is a helper function which filter an array according to string (filter).
Also , filter is not a redux-action, it is just string that decides todos to be rendered.
I may agree with you it is something weird , and if we can see the whole application (reducers, actions,... ) we can decide if it is best practices or not .
todos in this component is a calculated property based on the state of the reducer, and it is not changing any state.
It's okay to transform properties comming from recuders that are used only by one component (they are called selectors). Imagine that you use todos in other components, you will not want to make changes in one component like filtering and seeing that in the other components. If this is the case, it's fine to do it.
Also, it is a good property of your reducer to store only the needed data. More state is more complexity in the app, and more overhead to calculate new states.
It seems to me that a function should do what its name says, nothing less, nothing more.
mapStateToProps() should just do that, ie "map", and should normally not call other functions.
I have been slowly converting my React+Flux apps to use Immutable.js data structures. I use the original, vanilla FB implementation of Flux.
One problem I have encountered is mixing component state with state received from Flux stores.
I keep all important business-logic state in stores. But my rule has been to keep UI-related state within components. Stores don't need to concern themselves if, for example, a dropdown menu is open or not, right?
The problem comes when an action is taken in a component that changes state in that same component's store. Let's say we have a component with a dropdown menu that is open. An item is selected from that dropdown menu. The action propagates to the ItemStore, the store emits a change, and the component gets new state from the store.
_onChange() {
this.setState(this._getState());
}
_getState() {
if(this.state === undefined) {
return {
data: Immutable.Map({
selectedItem: ItemStore.getSelectedItem(),
items: ItemStore.getItems(),
menuIsOpen: false
})
};
}
return {
data: this.state.data.merge(Immutable.Map({
selectedItem: ItemStore.getSelectedItem(),
items: ItemStore.getItems(),
menuIsOpen: this.state.data.get("menuIsOpen")
}))
};
}
Concurrently, in the component, the click on the dropdown menu item emits an old-fashioned onClick event. I have a _handleClick function which uses setState to close the dropdown menu (local state).
_handleClick(event) {
event.preventDefault();
this.setState({
data: this.state.data.set("menuIsOpen", !this.state.data.get("menuIsOpen"))
});
}
The problem is that _handleClick ends up being called so soon after _getState that it doesn't have an updated copy of this.state.data. So in the component's render method, this.state.data.get("selectedItem") still shows the previously-selected item.
When I do this with POJOs, React's setState seems to batch everything correctly, so it was never an issue. But I don't want to have state that is not part of an Immutable.Map, because I want to take advantage of "pure" rendering. Yet I don't want to introduce UI state into my stores, because I feel like that could get messy real quick.
Is there a way I could fix this? Or is it just a bad practice to merge local Immutable.Map state and Immutable.Map store state within a single component?
RELATED: I am not a fan of the boilerplate if(this.state === undefined) logic to set initial local menuIsOpen state in my _getState method. This may be a sign that I am trying to do something that is not correct.
You can pass a callback to setState to enqueue an atomic update.
_onChange() {
this.setState(state => this._getState(state));
}
_getState(state) {
if(state === undefined) {
return {
data: Immutable.Map({
selectedItem: ItemStore.getSelectedItem(),
items: ItemStore.getItems(),
menuIsOpen: false
})
};
}
return {
data: state.data.merge(Immutable.Map({
selectedItem: ItemStore.getSelectedItem(),
items: ItemStore.getItems(),
menuIsOpen: state.data.get("menuIsOpen")
}))
};
}
About your related point, you might want to take a look at getInitialState.
Why have 2 separate actions occur when you click (fire action to store, close menu)? Instead, you could say, when they click a menu item, we need to close the menu item, and alert the store of the new value.
_handleClick(event) {
event.preventDefault();
this.setState({
data: this.state.data.set("menuIsOpen", false)
}, function() {
alertTheSelectionChange(selectedItem)
});
}