I have a PageContext that is holding the state of User objects as array. Each User object contains a ScheduledPost object that does mutate when user decides to add a new post. I have no idea how to trigger an update on my PageContext when it happens (I want to avoid forceUpdate() call). I need to somehow be notified of that, in order to re-render posts, maintain timer etc.
Please, see the code:
class User {
name: string;
createTime: number;
scheduledPosts: ScheduledPost[] = [];
/*
* Creates a new scheduled post
*/
public createScheduledPost(title : string, content : string, date : number): void {
this.scheduledPosts.push(Object.assign(new ScheduledPost(), {
title,
content,
date
}));
}
}
class ScheduledPost {
title: string;
content: string;
date: number;
public load(): void {
// Create timers etc.
}
public publish(): void {
// Publish post
}
}
// PageContext/index.tsx
export default React.createContext({
users: [],
editingUser: null,
setEditingUser: (user: User) => {}
});
// PageContextProvider.tsx
const PageContextProvider: React.FC = props => {
const [users, setUsers] = useState<User[]>([]);
const [editingUser, setEditingUser] = useState<User>(null);
// Load users
useEffect(() => {
db.getUsers()
.then(result => setUsers(result));
}, []);
return (
<PageContext.Provider value={{
users,
editingUser,
setEditingUser
}}>
{props.children}
</PageContext.Provider>
);
};
What I would like to achieve is, when consuming my provider with useContext hook:
const ctx = useContext(PageContext);
I would like to create a schedule post from any component like so:
// Schedule post (1 hour)
ctx.editingUser.createScheduledPost("My post title", "The post content", (new Date).getTime() + 1 * 60 * 60);
However, this wont work, since React doesn't know that User property has just mutated.
Questions:
How can I make React being notified of the changes within any of the User object instance? What is the way to solve it properly (excluding forceUpdate)?
Am I doing it right? I'm new to React and I feel like the structure I'm using here is cumbersome and just not right.
Where are the users being mutated? If you're storing them in your state as it appears, the changes should be detected. However if you're using the methods built into the User class to let them directly update themselves, then React will not pick up on them. You would need to update the entire users array in your state to make sure React can respond to the changes.
It's tough to give a more specific example without seeing exactly where/how you're updating your users currently, but a generalized mutation might go something like this (you can still use a class method, if desired):
const newUsers = Array.from(users); // Deep copy users array to prevent direct state mutation.
newUsers[someIndex].createScheduledPost(myTitle, myContent, myDate);
setUsers(newUsers); // Calling the setX function tied to your useState call will automatically trigger updates/re-renders for all (unless otherwise specified) components/operations that depend on it
In React re-render is caused by calling setState within component (or by using hooks, but the point is that you need to call specific method) or by changing component props. That means that manual mutation of your state will never cause a re-render - even if you had simple component and called
this.state.something = somethingElse;
re-render would not occur. Same thing works for context.
For your case, this means that you should not mutate editingUser state, but call setEditingUser with changed user state, something like:
const user = { ...ctx.editingUser };
user.createScheduledPost("My post title", "The post content", (new Date).getTime() + 1 * 60 * 60);
ctx.setEditingUser(user);
I'm not sure about your inner structure, but if that same user is also in users array, then you'll need to update that part of state by calling setUsers method where you maintain whole array and only update that single user which changed data - if thats the case then I'd think about restructuring the app because it already gets complicated for such simple state changes. You should also consider using redux, mobx or some other state management library instead of react context (my personal advice).
EDIT
Please take a look at this quite:
In a typical React application, data is passed top-down (parent to
child) via props, but this can be cumbersome for certain types of
props (e.g. locale preference, UI theme) that are required by many
components within an application. Context provides a way to share
values like these between components without having to explicitly pass
a prop through every level of the tree.
As you can see, react team is suggesting using context for some global preferences that are required within many components. The main problem with using context (in my opinion) is that you don't write natural react components - they don't receive dependant data through props but rather from within the context api itself. This means that you won't be able to reuse your components without also integrating context part of application.
While for example redux has similar concept of keeping state at one place, it still propagades that state (and its changes) to components via props, making your components undependent of both redux, context or anything else.
You can stick to react context and make whole app work with it, but I'm just saying it wouldn't be best practice to do so.
Related
I understand the benefits of using a store pattern and having a single source of truth for data shared across components in an application, and making API calls in a store action that gets called by components rather than making separate requests in every component that requires the data.
It's my understanding that if this data needs to change in some way, depending on the component using the data, this data can be updated by calling a store action with the appropriate filters/args, and updating the global store var accordingly.
However, I am struggling to understand how to solve the issue whereby a parent component requires one version of this data, and a child of that component requires another.
Consider the following example:
In an API, there exists a GET method on an endpoint to return all people. A flag can be passed to return people who are off sick:
GET: api/people returns ['John Smith', 'Joe Bloggs', 'Jane Doe']
GET: api/people?isOffSick=true returns ['Jane Doe']
A parent component in the front end application requires the unfiltered data, but a child component requires the filtered data. For arguments sake, the API does not return the isOffSick boolean in the response, so 2 separate requests need to be made.
Consider the following example in Vue.js:
// store.js
export const store = createStore({
state: {
people: []
},
actions: {
fetchPeople(filters) {
// ...
const res = api.get('/people' + queryString);
commit('setPeople', res.data);
}
},
mutations: {
setPeople(state, people) {
state.people = people;
}
}
});
// parent.vue - requires ALL people (NO filters/args passed to API)
export default {
mounted() {
this.setPeople();
},
computed: {
...mapState([
'people'
])
},
methods: {
...mapActions(['setPeople']),
}
}
// child.vue - requires only people who are off sick (filters/args passed to API)
export default {
mounted() {
this.setPeople({ isOffSick: true });
},
computed: {
...mapState([
'people'
])
},
methods: {
...mapActions(['setPeople']),
}
}
The parent component sets the store var with the data it requires, and then the child overwrites that store var with the data it requires.
Obviously the shared store var is not compatible with both components.
What is the preferred solution to this problem for a store pattern? Storing separate state inside the child component seems to violate the single source of truth for the data, which is partly the reason for using a store pattern in the first place.
Edit:
My question is pertaining to the architecture of the store pattern, rather than asking for a solution to this specific example. I appreciate that the API response in this example does not provide enough information to filter the global store of people, i.e. using a getter, for use in the child component.
What I am asking is: where is an appropriate place to store this second set of people if I wanted to stay true to a store focused design pattern?
It seems wrong somehow to create another store variable to hold the data just for the child component, yet it also seems counter-intuitive to store the second set of data in the child component's state, as that would not be in line with a store pattern approach and keeping components "dumb".
If there were numerous places that required variations on the people data that could only be created by a separate API call, there would either be a) lots of store variables for each "variation" of the data, or b) separate API calls and state in each of these components.
Thanks to tao I've found what I'm looking for:
The best approach would be to return the isOffSick property in the API response, then filtering the single list of people (e.g. using a store getter), thus having a single source of truth for all people in the store and preventing the need for another API request.
If that was not possible, it would make sense to add a secondary store variable for isOffSick people, to be consumed by the child component.
I have an app. that uses NextJS. I have a page that looks like the following:
import React from 'react'
import { parseQuery } from '../lib/searchQuery'
import Search from '../components/search'
class SearchPage extends React.Component {
static getInitialProps ({ query, ...rest }) {
console.log('GET INITIAL PROPS')
const parsedQuery = parseQuery(query)
return { parsedQuery }
}
constructor (props) {
console.log('CONSTRUCTOR OF PAGE CALLED')
super(props)
this.state = props.parsedQuery
}
render () {
return (
<div>
<div>
<h1>Search Results</h1>
</div>
<div>
<h1>DEBUG</h1>
<h2>PROPS</h2>
{JSON.stringify(this.props)}
<h2>STATE</h2>
{JSON.stringify(this.state)}
</div>
<div>
<Search query={this.state} />
</div>
</div>
)
}
}
export default SearchPage
getInitialProps is ran for SSR - it receives the query string as an object (via Express on the back end) runs it through a simple 'cleaner' function - parseQuery - which I made, and injects it into the page via props as props.parsedQuery as you can see above. This all works as expected.
The Search component is a form with numerous fields, most of which are select based with pre-defined fields and a few a number based input fields, for the sake of brevity I've omitted the mark up for the whole component. Search takes the query props and assigns them to its internal state via the constructor function.
On changing both select and input fields on the Search component this code is ran:
this.setState(
{
[label]: labelValue
},
() => {
if (!this.props.homePage) {
const redirectObj = {
pathname: `/search`,
query: queryStringWithoutEmpty({
...this.state,
page: 1
})
}
// Router.push(href, as, { shallow: true }) // from docs.
this.props.router.push(redirectObj, redirectObj, { shallow: true })
}
}
)
The intention here is that CSR takes over - hence the shallow router.push. The page URL changes but getInitialProps shouldn't fire again, and subsequent query changes are handled via componentWillUpdate etc.. I confirmed getInitialProps doesn't fire again by lack of respective console.log firing.
Problem
However, on checking/unchecking the select fields on the Search component I was surprised to find the state of SearchPage was still updating, despite no evidence of this.setState() being called.
constructor isn't being called, nor is getInitialProps, so I'm unaware what is causing state to change.
After initial SSR the debug block looks like this:
// PROPS
{
"parsedQuery": {
"manufacturer": [],
"lowPrice": "",
"highPrice": ""
}
}
// STATE
{
"manufacturer": [],
"lowPrice": "",
"highPrice": ""
}
Then after checking a select field in Search surprisingly it updates to this:
// PROPS
{
"parsedQuery": {
"manufacturer": ["Apple"],
"lowPrice": "",
"highPrice": ""
}
}
// STATE
{
"manufacturer": ["Apple"],
"lowPrice": "",
"highPrice": ""
}
I can't find an explanation to this behaviour, nothing is output to the console and I can't find out how to track state changes origins via dev. tools.
Surely the state should only update if I were to do so via componentDidUpdate? And really shouldn't the parsedQuery prop only ever be updated by getInitialProps? As that's what created and injected it?
To add further confusion, if I change a number input field on Search (such as lowPrice), the URL updates as expected, but props nor page state changes in the debug block. Can't understand this inconsistent behaviour.
What's going on here?
EDIT
I've added a repo. which reproduces this problem on as a MWE on GitHub, you can clone it here: problem MWE repo.
Wow, interesting problem. This was a fun little puzzle to tackle.
TL;DR: This was your fault, but how you did it is really subtle. First things first, the problem is on this line:
https://github.com/benlester/next-problem-example/blob/master/frontend/components/search.js#L17
Here in this example, it is this:
this.state = props.parsedQuery
Let's consider what is actually happening there.
In IndexPage.getInitialProps you are doing the following:`
const initialQuery = parseQuery({ ...query })
return { initialQuery }
Through Next's mechanisms, this data passes through App.getInitialProps to be returned as pageProps.initialQuery, which then becomes props.initialQuery in IndexPage, and which is then being passed wholesale through to your Search component - where your Search component then "makes a copy" of the object to avoid mutating it. All good, right?
You missed something.
In lib/searchQuery.js is this line:
searchQuery[field] = []
That same array is being passed down into Search - except you aren't copying it. You are copying props.query - which contains a reference to that array.
Then, in your Search component you do this when you change the checkbox:
const labelValue = this.state[label]
https://github.com/benlester/next-problem-example/blob/master/frontend/components/search.js#L57
You're mutating the array you "copied" in the constructor. You are mutating your state directly! THIS is why initialQuery appears to update on the home page - you mutated the manufacturers array referenced by initialQuery - it was never copied. You have the original reference that was created in getInitialProps!
One thing you should know is that even though getInitialProps is not called on shallow pushes, the App component still re-renders. It must in order to reflect the route change to consuming components. When you are mutating that array in memory, your re-render reflects the change. You are NOT mutating the initialQuery object when you add the price.
The solution to all this is simple. In your Search component constructor, you need a deep copy of the query:
this.state = { ...cloneDeep(props.query) }
Making that change, and the issue disappears and you no longer see initialQuery changing in the printout - as you would expect.
You will ALSO want to change this, which is directly accessing the array in your state:
const labelValue = this.state[label]
to this:
const labelValue = [...this.state[label]]
In order to copy the array before you change it. You obscure that problem by immediately calling setState, but you are in fact mutating your component state directly which will lead to all kinds of weird bugs (like this one).
This one arose because you had a global array being mutated inside your component state, so all those mutations were being reflected in various places.
I am learning how redux works but its a lot of code to do simple things. For example, I want to load some data from the server before displaying. For editing reasons, I can't simply just use incoming props but I have to copy props data into the local state.
As far as I've learned, I have to send a Fetch_request action. If successful, a fetch_success action will update the store with new item. Then updated item will cause my component's render function to update.
In component
componentWillMount() {
this.props.FETCH_REQUEST(this.props.match.params.id);
}
...
In actions
export function FETCH_REQUEST(id) {
api.get(...)
.then(d => FETCH_SUCCESS(d))
.catch(e => FETCH_FAILURE(e));
}
...
In reducer
export function FETCH_REDUCER(state = {}, action ={}) {
switch (action.type) {
case 'FETCH_SUCCESS':
return { ...state, [action.payload.id]: ...action.payload }
...
}
Back in component
this.props.FETCH_REDUCER
// extra code for state, getting desired item from...
Instead, can I call a react-thunk function and pass some callback functions? The react-thunk can update the store and callbacks can change the component's local state.
In component
componentWillMount() {
this.props.FETCH_REQUEST(this.props.match.params.id, this.cbSuccess, this.cbFailure);
}
cbSuccess(data) {
// do something
}
cbFailure(error) {
// do something
}
...
In action
export function FETCH_REQUEST(id, cbSuccess, cbFailure) {
api.get(...)
.then(d => {
cbSuccess(d);
FETCH_SUCCESS(d);
}).catch(e => {
cbFailure(d);
FETCH_FAILURE(e);
});
}
...
Is this improper? Can I do the same thing with redux-observable?
UPDATE 1
I moved nearly everything to the redux store, even for edits (ie replaced this.setState with this.props.setState). It eases state management. However, every time any input's onChange fires, a new state is popping up. Can someone confirm whether this is okay? I'm worried about the app's memory management due to redux saving a ref to each state.
First of all, you should call your API in componentDidMount instead of componentWillMount. More on this at : what is right way to do API call in react js?
When you use a redux store, your components subscribe to state changes using the mapStateToProps function and they change state using the actions added a props through the mapDispatchToProps function (assuming you are using these functions in your connect call).
So you already are subscribing to state changes using your props. Using a callback would be similar to having the callback tell you of a change which your component already knows about because of a change in its props. And the change in props would trigger a re-render of the component to show the new state.
UPDATE:
The case you refer to, of an input field firing an onChange event at the change of every character, can cause a lot of updates to the store. As mentioned in my comments, you can use an api like _.debounce to throttle the updates to the store to reduce the number of state changes in such cases. More on handling this at Perform debounce in React.js.
The issue of memory management is a real issue in real world applications when using Redux. The way to reduce the effect of repeated updates to the state is to
Normalize the shape of state : http://redux.js.org/docs/recipes/reducers/NormalizingStateShape.html
Create memoized selectors using Reselect (https://github.com/reactjs/reselect)
Follow the advice provided in the articles regarding performance in Redux github pages (https://github.com/reactjs/redux/blob/master/docs/faq/Performance.md)
Also remember that although the whole state should be copied to prevent mutating, only the slice of state that changes needs to be updated. For example, if your state holds 10 objects and only one of them changes, you need to update the reference of the new object in the state, but the remaining 9 unchanged objects still point to the old references and the total number of objects in your memory is 11 and not 20 (excluding the encompassing state object.)
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 5 years ago.
Improve this question
I'm new to React/Redux. I use a fetch api middleware in Redux app to process the APIs. It's (redux-api-middleware). I think it's the good way to process async api actions. But I find some cases which can't be resolve by myself.
As the homepage (Lifecycle) say, a fetch API lifecycle begins with dispatching a CALL_API action ends with dispatching a FSA action.
So my first case is showing/hiding a preloader when fetching APIs. The middleware will dispatch a FSA action at the beginning and dispatch a FSA action at the end. Both the actions are received by reducers which should be only doing some normal data processing. No UI operations, no more operations. Maybe I should save the processing status in state then render them when store updating.
But how to do this? A react component flow over the whole page? what happen with store updating from other actions? I mean they are more like events than state!
Even a worse case, what should I do when I have to use the native confirm dialog or alert dialog in redux/react apps? Where should they be put, actions or reducers?
Best wishes! Wish for replying.
I mean they are more like events than state!
I would not say so. I think loading indicators are a great case of UI that is easily described as a function of state: in this case, of a boolean variable. While this answer is correct, I would like to provide some code to go along with it.
In the async example in Redux repo, reducer updates a field called isFetching:
case REQUEST_POSTS:
return Object.assign({}, state, {
isFetching: true,
didInvalidate: false
})
case RECEIVE_POSTS:
return Object.assign({}, state, {
isFetching: false,
didInvalidate: false,
items: action.posts,
lastUpdated: action.receivedAt
The component uses connect() from React Redux to subscribe to the store’s state and returns isFetching as part of the mapStateToProps() return value so it is available in the connected component’s props:
function mapStateToProps(state) {
const { selectedReddit, postsByReddit } = state
const {
isFetching,
lastUpdated,
items: posts
} = postsByReddit[selectedReddit] || {
isFetching: true,
items: []
}
return {
selectedReddit,
posts,
isFetching,
lastUpdated
}
}
Finally, the component uses isFetching prop in the render() function to render a “Loading...” label (which could conceivably be a spinner instead):
{isEmpty
? (isFetching ? <h2>Loading...</h2> : <h2>Empty.</h2>)
: <div style={{ opacity: isFetching ? 0.5 : 1 }}>
<Posts posts={posts} />
</div>
}
Even a worse case, what should I do when I have to use the native confirm dialog or alert dialog in redux/react apps? Where should they be put, actions or reducers?
Any side effects (and showing a dialog is most certainly a side effect) do not belong in reducers. Think of reducers as passive “builders of state”. They don’t really “do” things.
If you wish to show an alert, either do this from a component before dispatching an action, or do this from an action creator. By the time an action is dispatched, it is too late to perform side effects in response to it.
For every rule, there is an exception. Sometimes your side effect logic is so complicated you actually want to couple them either to specific action types or to specific reducers. In this case check out advanced projects like Redux Saga and Redux Loop. Only do this when you are comfortable with vanilla Redux and have a real problem of scattered side effects you’d like to make more manageable.
Great answer Dan Abramov!
Just want to add that I was doing more or less exactly that in one of my apps (keeping isFetching as a boolean) and ended up having to make it an integer (which ends up reading as the number of outstanding requests) to support multiple simultaneous requests.
with boolean:
request 1 starts -> spinner on -> request 2 starts -> request 1 ends -> spinner off -> request 2 ends
with integer:
request 1 starts -> spinner on -> request 2 starts -> request 1 ends -> request 2 ends -> spinner off
case REQUEST_POSTS:
return Object.assign({}, state, {
isFetching: state.isFetching + 1,
didInvalidate: false
})
case RECEIVE_POSTS:
return Object.assign({}, state, {
isFetching: state.isFetching - 1,
didInvalidate: false,
items: action.posts,
lastUpdated: action.receivedAt
I'd like to add something. The real world example uses a field isFetching in the store to represent when a collection of items is being fetched. Any collection is generalized to a pagination reducer that can be connected to your components to track the state and show if a collection is loading.
It happened to me that I wanted to fetch details for an specific entity that doesn't fit in the pagination pattern. I wanted to have a state representing if the details are being fetched from the server but also I didn't want to have a reducer just for that.
To solve this I added another generic reducer called fetching. It works in a similar fashion to the pagination reducer and it's responsibility is just to watch a set of actions and generate new state with pairs [entity, isFetching]. That allows to connect the reducer to any component and to know if the app is currently fetching data not just for a collection but for an specific entity.
I didn't happen upon this question until now, but since no answer is accepted I'll throw in my hat. I wrote a tool for this very job: react-loader-factory. It's got slightly more going on than Abramov's solution, but is more modular and convenient, since I didn't want to have to think after I wrote it.
There are four big pieces:
Factory pattern: This allows you to quickly call the same function to set up which states mean "Loading" for your component, and which actions to dispatch. (This assumes that the component is responsible for starting the actions it waits on.) const loaderWrapper = loaderFactory(actionsList, monitoredStates);
Wrapper: The component the Factory produces is a "higher order component" (like what connect() returns in Redux), so that you can just bolt it onto your existing stuff. const LoadingChild = loaderWrapper(ChildComponent);
Action/Reducer interaction: The wrapper checks to see if a reducer it's plugged into contains keywords that tell it not to pass through to the component that needs data. The actions dispatched by the wrapper are expected to produce the associated keywords (the way redux-api-middleware dispatches ACTION_SUCCESS and ACTION_REQUEST, for example). (You could dispatch actions elsewhere and just monitor from the wrapper if you wanted, of course.)
Throbber: The component you want to appear while the data your component depends on isn't ready. I added a little div in there so you can test it out without having to rig it up.
The module itself is independent of redux-api-middleware, but that's what I use it with, so here's some sample code from the README:
A component with a Loader wrapping it:
import React from 'react';
import { myAsyncAction } from '../actions';
import loaderFactory from 'react-loader-factory';
import ChildComponent from './ChildComponent';
const actionsList = [myAsyncAction()];
const monitoredStates = ['ASYNC_REQUEST'];
const loaderWrapper = loaderFactory(actionsList, monitoredStates);
const LoadingChild = loaderWrapper(ChildComponent);
const containingComponent = props => {
// Do whatever you need to do with your usual containing component
const childProps = { someProps: 'props' };
return <LoadingChild { ...childProps } />;
}
A reducer for the Loader to monitor (although you can wire it differently if you want):
export function activeRequests(state = [], action) {
const newState = state.slice();
// regex that tests for an API action string ending with _REQUEST
const reqReg = new RegExp(/^[A-Z]+\_REQUEST$/g);
// regex that tests for a API action string ending with _SUCCESS
const sucReg = new RegExp(/^[A-Z]+\_SUCCESS$/g);
// if a _REQUEST comes in, add it to the activeRequests list
if (reqReg.test(action.type)) {
newState.push(action.type);
}
// if a _SUCCESS comes in, delete its corresponding _REQUEST
if (sucReg.test(action.type)) {
const reqType = action.type.split('_')[0].concat('_REQUEST');
const deleteInd = state.indexOf(reqType);
if (deleteInd !== -1) {
newState.splice(deleteInd, 1);
}
}
return newState;
}
I expect in the near future I'll add things like timeout and error to the module, but the pattern's not going to be very different.
The short answer to your question is:
Tie rendering to rendering code--use a wrapper around the component you need to render with the data like the one I showed above.
Add a reducer that makes the status of requests around the app you might care about easily digestible, so you don't have to think too hard about what is happening.
Events and state aren't really different.
The rest of your intuitions seem correct to me.
Am I the only one thinking that loading indicators don't belong in a Redux store? I mean, I don't think it's part of an application's state per se..
Now, I work with Angular2, and what I do is that I have a "Loading" service which exposes different loading indicators via RxJS BehaviourSubjects.. I guess the mechanism is the same, I just don't store the information in Redux.
Users of the LoadingService just subscribe to those events they want to listen to..
My Redux action creators call the LoadingService whenever things need to change. UX components subscribe to the exposed observables...
You can add change listeners to your stores, using either connect() from React Redux or the low-level store.subscribe() method. You should have the loading indicator in your store, which the store change handler can then check and update the component state. The component then renders the preloader if needed, based on the state.
alert and confirm shouldn't be a problem. They are blocking and alert doesn't even take any input from the user. With confirm, you can set state based on what the user has clicked if the user choice should affect component rendering. If not, you can store the choice as component member variable for later use.
We have three types of notifications in our app, all of which are designed as aspects:
Loading indicator (modal or non-modal based on prop)
Error Popup (modal)
Notification snackbar (non-modal, self closing)
All three of these are at the top level of our app (Main), and wired through Redux as shown in the below code snippet. These props control display of their corresponding aspects.
I designed a proxy that handles all our API calls, thus all isFetching and (api) errors are mediated with actionCreators I import in the proxy. (As an aside, I also use webpack to inject a mock of the backing service for dev so we can work without server dependencies.)
Any other place in the app that needs to provide any type of notification simply imports the appropriate action. Snackbar & Error have params for messages to be displayed.
#connect(
// map state to props
state => ({
isFetching :state.main.get('isFetching'), // ProgressIndicator
notification :state.main.get('notification'), // Snackbar
error :state.main.get('error') // ErrorPopup
}),
// mapDispatchToProps
(dispatch) => { return {
actions: bindActionCreators(actionCreators, dispatch)
}}
)
export default class Main extends React.Component{
I'm saving the urls such as::
isFetching: {
/api/posts/1: true,
api/posts/3: false,
api/search?q=322: true,
}
And then I have a memorised selector (via reselect).
const getIsFetching = createSelector(
state => state.isFetching,
items => items => Object.keys(items).filter(item => items[item] === true).length > 0 ? true : false
);
To make the url unique in case of POST, I pass some variable as query.
And where I want to show an indicator, I simply use the getFetchCount variable
I'm working on a page whose 'Data Model' is a collection, for example, an array of people. They are packed into React Components and tiled on the page. Essentially it's like:
class App extends React.Component {
constructor() {
super();
this.state = { people: /* some data */ };
}
render () {
return (
<div>
{this.state.people.map((person) =>
<People data={person}></People>)}
</div>);
}
}
Now I want to attach an edit section for each entry in <People> component, which allows the user to update the name, age ... all kinds of information for a specific entry.
Since React does not support mutating props inside components, I searched and found that adding callbacks as props can solve the problem of passing data to parent. But since there are many fields to update, there would be many callbacks such as onNameChanged, onEmailChanged... which could be very ugly (also more and more verbose as the number of fields keeps growing).
So what is the right way for it?
Honestly? The best way is Flux (back to that in a minute).
If you start to get into the process of passing data down the tree in the form of props, then passing it back up to be edited using callbacks, then you're breaking the unidirectional data flow that React is built around.
However, not all projects need to be written to ideal standards and it is possible to build this without Flux (and sometimes it might even be the right solution).
Without Flux
You can implement this without the need for a mass of callbacks, by passing down a single edit function as a prop. This function should take an id and a new person object, then update the state inside the parent component whenever it runs. Here's an example.
editPerson(id, editedPerson) {
const people = this.state.people;
const newFragment = { [id]: editedPerson };
// create a new list of people, with the updated person in
this.setState({
people: Object.assign([], people, newFragment)
});
},
render() {
// ...
{this.state.people.map((person, index) => {
const edit = this.editPerson.bind(this, index);
return (
<People data={person} edit={edit}></People>
);
})}
// ...
}
Then inside your person component, any time you make a change to the person, simply pass the person back up to the parent state with the callback.
However, if you visualize the flow of data through your application, you've now created a cycle that looks something like this.
App
^
|
v
Person
It's no longer trivial to work out where the data in app came from (it is still quite simple in such a small app, but obviously the bigger it gets the harder it is to tell.
With Flux
In the beginning, Facebook developers wrote React applications with unidirectional data flows and they saw that it was good. However, a need arose for data to go up the tree, which resulted in a crisis. How shall our data flow be unidirectional and still return to the top of the tree? And on the seventh day, they created Flux(1) and saw that it was very good.
Flux allows you to describe your changes as actions and pass them out of your components, to stores (self contained state boxes) which understand how to manipulate their state based on the action. Then the store tells all the components that care about it that something has changed, at which point the components can fetch new data to render.
You regain your unidirectional data flow, with an architecture that looks like this.
App <---- [Stores]
| ^
v |
Person --> Dispatcher
Stores
Rather than keeping your state in your <App /> component, you would probably want to create a People store to keep track of your list of people.
Maybe it would look something like this.
// stores/people-store.js
const people = [];
export function getPeople() {
return people;
}
function editPerson(id, person) {
// ...
}
function addPerson(person) {
// ...
}
function removePerson(id) {
// ...
}
Now, we could export these functions and let our components call them directly, but that's bad because it means that our components have to have knowledge of the design of the store and we want to keep them as dumb as possible.
Actions
Instead, our components create simple, serializable actions that our stores can understand. Here are some examples:
// remove person with id 53
{ type: 'PEOPLE_REMOVE', payload: 53 }
// create a new person called John Foo
{ type: 'PEOPLE_ADD', payload: { name: 'John Foo' } }
// edit person 13
{
type: 'PEOPLE_EDIT',
payload: {
id: 13,
person: { name: 'Unlucky Bill' }
}
}
These actions don't have to have these specific keys, they don't even have to be objects either, this is just the convention from Flux Standard Actions.
Dispatcher
Now, we have tell our store how to deal with these actions when they arrive.
// stores/people-store.js
// ...
dispatcher.register(function(action) {
switch(action.type) {
case 'PEOPLE_REMOVE':
removePerson(action.payload);
case 'PEOPLE_ADD':
addPerson(action.payload);
case 'PEOPLE_EDIT':
editPerson(action.payload.id, action.payload.person);
}
});
Phew. Lot of work so far, nearly there.
Now we can start to dispatch these actions from our components.
// components/people.js
// ...
onEdit(editedPerson) {
dispatcher.dispatch({
type: 'PEOPLE_EDIT',
payload: {
id: this.props.id,
person: editedPerson
}
});
}
onRemove() {
dispatcher.dispatch({
type: 'PEOPLE_REMOVE',
payload: this.props.id
});
}
// ...
When you edit the person, call the this.onEdit method and it will dispatch the appropriate action to your stores. Same goes for removing a person. Normally you'd move this stuff into action creators, but that's a topic for another time.
Ok, finally getting somewhere! Now our components can create actions that update the data in our stores. How do we get that data back into our components?
Initially, it's very simple. We can require the store in our top level component and simply ask for the data.
// components/app.js
import { getPeople } from './stores/people-store';
// ...
constructor() {
super();
this.state = { people: getPeople() };
}
We can pass this data down in exactly the same way, but what happens when the data changes?
The official stance from Flux is basically "Not our problem". Their examples use Node's Event Emitter class to allow stores to accept callback functions that are called when the store updates.
This allows you to write code that looks something like this:
componentWillMount() {
peopleStore.addListener(this.peopleUpdated);
},
componentWillUnmount() {
peopleStore.removeListener(this.peopleUpdated);
},
peopleUpdated() {
this.setState({ people: getPeople() });
}
Really, the ball is in your court on this one. There are many other strategies for getting the data back into your program. Reflux creates the listen method for you automatically, Redux allows you to declaratively specify which components receive which parts of the store as props, then it handles the updating. Spend enough time with Flux and you'll find a preference.
Now, you're probably thinking, blimey — this seems like a lot of effort to go to just to add edit functionality to a component; and you're right, it is!
For small applications, you probably don't need Flux.
Sure there are lots of benefits, but the additional complexity just isn't always warranted. As your application grows, you'll find that if you've fluxed it up, it will be much easier to manage, maintain and debug.
The trick is to know when it's appropriate to use the Flux architecture and hopefully when the time comes, this overly long, rambling answer will have cleared things up for you.
This isn't actually true.