React - most efficient way to share data between components - reactjs

I'll start by saying I'm very new to React and am just playing around with having components interact with each other... trying to get a sense for where state belongs and the most efficient way(s) to render changes on screen.
I have 2 sibling components, Bro and Sis that are direct children of Dad. Bro makes an HTTP request in componentWillMount to get initial values for its state. It then passes one of the pieces of data from the response (uid) back up to Dad (via a method defined in Dad) which is then passed down to Sis via props. Sis then uses this value in making ITS initial HTTP request (in componentDidUpdate) to populate ITS state.
Dad
class Dad extends Component {
state = {
uid: null
}
updateUID = id => {
this.setState({uid: id});
}
}
render() {
return (
<>
<Bro />
<Sis update={this.updateUID} />
</>
);
}
Sis
class Sis extends Component {
state = {
uid: null,
something: null,
another: null
}
componentDidUpdate() {
axios.get('example.com/endpoint2.json')
.then(res => {
/*
transform as needed and put the vales from
res.data into this.state accordingly...
*/
});
}
render () {
return <section>Component: Sis</section>;
}
}
Bro
class Bro extends Component {
state = {
uid: null,
blah: null,
blah-blah: null
}
componentWillUpdate() {
axios.get('example.com/endpoint1.json')
.then(res => {
/*
...
transform as needed and put the vales from
res.data into this.state accordingly...
*/
// pass uid back up to Dad to be passed down to Sis
this.props.update(res.data.uid);
});
}
render () {
return <section>Component: Bro</section>;
}
}
Is this Bro --> Dad --> Sis passing of data the right way to do this? This seems a bit slow and perhaps unnecessarily complicated to me... I think. The alternate ways i can think of doing it are:
have Sis make its initial HTTP request in componentWillMount and fetch the value of uid on its own. This would eliminate the need to pass it from one child to the parent to the other child, but it would involve a partially redundant query on the backend which is why I chose not to go this route.
have Dad make an HTTP request that performs 1 combined query to return the data needed by both Bro and Sis and pass it down to each accordingly. As it stands right now, Dad does not always display Bro and Sis (depending on the route). In those cases, it would be a useless HTTP request and thus definitely not right, but I'm thinking a bit of restructuring may make this viable...
perhaps nesting Dad in something like Grandpa and letting Grandpa take care of the routing while Dad fetches the data for Bro and Sis.
So I guess ultimately my question is: should I be passing data between child/adjacent/sibling components via their parent component or should the parent component be the source of the data for both children and pass it down to each accordingly?

First of all, you shouldn't be calling an HTTP request in componentWillMount(). Instead do so in componentDidMount() as stated in React docs
Your method is complete fine. However based on the container/presentational (smart/dump) components strategy you'd better do all your data fetching in <Dad /> component, then pass down the required data to the children. This way it would be so much easier to keep track of your requests and your data won't be scattered about.
An alternative is to use 3rd-party libraries such as Redux or Mobx State Tree. I'm not sure about Mobx, but what Redux does is it keeps the state outside of the components and make it available to the whole application by React context. You should be thinking about using this as it's extremely powerful and easy to learn
Last but no least, I will include a couple of posts here about container/presentational components pattern:
From Dan Abramov - The creator of Redux
Another medium post

Related

React-native ComponentDidMount not firing

i tried to update the sound array which i imported from other component every time it is changed. But however, it only fire componentDidMount() only once and it won't run again. Down below is my code on the problem:
//sound array from another component
import { soundArray } from "./CreateRecord";
export default class RecordingList extends React.Component {
constructor() {
super();
this.currentSoundArray = [];
}
componentDidMount() {
console.log(this.currentSoundArray);
this.getCurrentArray();
}
getCurrentArray() {
this.currentSoundArray = soundArray;
}
render(){
...
}
currently when i view the component, the componentDidMound will run once and console the sound array. At first, the sound array is empty:
[]
However, after i put value in the sound array and comeback to view the component, it wont print the console and it won't update the value of this.currentSoundArray
My expected result should be the currentSoundArray will be changed and print to the console every time the soundArray has been changed in another component. for example:
[]
[1,2]
[1,2,4]
componentDidMount() is invoked immediately after a component is mounted (inserted into the tree). Initialization that requires DOM nodes should go here. If you need to load data from a remote endpoint, this is a good place to instantiate the network request.
It runs only once.
What you are trying to do is access currentSoundArray value when it is updated from another component, now that you will not be able to do traditionally.
Also componentDidMount fires only once when the component is first initialized and rendered.
Solution 1
A better way of doing this is using something like React Redux to manage your application state, this way you would be able to access the states from any component throughout your application.
I have just finished setting up a boiler plate template for this exact thing, if you would like to check it out its on Github :)
Solution 2
If you are not interested in Redux then i would suggest you use react context for this, it will solve your issue as well. you can check out many examples online for example this uses context to share a snackbar across components.
Hope this Helps!

Using this.props.history.push("/path") is re-rendering and then returning

Edited the question after further debugging
I am having a strange issue, tried for a while to figure it out but I can't.
I have a React Component called NewGoal.jsx, after a user submits their new goal I attempt to reroute them to my "goals" page.
The problem: After they submit the browser loads in my goal page, but only for one second. It then continues and goes BACK to the NewGoal page!!
I am trying to understand why this is happening, I am beginning to feel that this might be an async issue.
Here is my code, currently it is using async-await, I also tried the same idea using a .then() but it also didn't work:
async handleSubmit(event)
{
const response = await axios.post("http://localhost:8080/addGoal",
{
goalID: null,
duration: this.state.days,
accomplishedDays: 0,
isPublic: this.state.isPublic,
description: this.state.name,
dateCreated: new Date().toISOString().substring(0,10),
}) */
// push to route
this.props.history.push("/goals");
}
While debugging, I tried taking out the functionality where I post the new message, and just did a history.push, code is below - and this completely worked.
// THIS WORKS
async handleSubmit(event)
{
// push to route
this.props.history.push("/goals");
}
But as soon as I add anything else to the function, whether before the history.push or after, it stops working.
Any advice would be very very appreciated!
Thank you
In the React Router doc's the developers talk about how the history object is mutable. Their recommendation is not to alter it directly.
https://reacttraining.com/react-router/web/api/history#history-history-is-mutable
Fortunately there are few ways to programmatically change the User's location while still working within the lifecycle events of React.
The easiest I've found is also the newest. React Router uses the React Context API to make the history object used by the router available to it's descendents. This will save you passing the history object down your component tree through props.
The only thing you need to do is make sure your AddNewGoalPage uses the history object from context instead of props.
handleSubmit(event)
...
//successful, redirect to all goals
if(res.data)
{
this.context.history.push("/goals")
}
...
})
}
I don't know if you're using a class component or a functional component for the AddNewGoalPage - but your handleSubmit method hints that it's a member of a Class, so the router's history object will be automatically available to you within your class through this.context.history.
If you are using a functional component, you'll need to make sure that the handleSubmit method is properly bound to the functional component otherwise the context the functional component parameter is given by React won't not be available to it.
Feel free to reply to me if this is the case.

React Architecture Redesign

Before we go any further: Redux or Flux is out of the question, not enough time to learn and properly test/implement/document
Here is my top level component. The problem is that I need a way to store state for the children, but they aren't being rendered here hence the need for restructuring.
-- Both of these are in the render/return functions of my top level component
<SideNav {...tab_props} {...this.props} />
...
{React.cloneElement(this.props.children, { callback: this.callback.bind(this) })}
I am seeking advice on how to best redesign our front-end with the primary goal of minimizing requests, and ultimately cacheing needed data in state somewhere in the app. Thoughts?
So, after much searching and research I stumbled on an awesome blog post that covers exactly what I need and it turns out I don't need to do the major restructuring that I initially thought was necessary.
Link > Blog Post
So, what I am going to do is pretty simple. React.cloneElement is going to be used here. I will be mapping through all of the children, which are being rendered based on the URL (using react-router), and passing to them the state as well as some callback functions (not created yet for this use-case)
Would look something like this, I also tested it and it works!
render() {
let childWithState = React.Children.map(this.props.children, (child) => {
return React.cloneElement(child, {state: this.state, callback: this.setParentState})
})
...
return(
{childWithState}
)
}
So, now from any of these children elements, I only need to call on this.props.state.--insert-something-- and I have access to is. If it doesn't exist, I can run my fetch request, and then in a callback function set the state in the parent, which would immediately re-render my component and give me access to the parent state!

Is it ok to mutate state directly before calling setState()?

I'm using a lightweight ORM to connect my react app with an external service... This package returns objects of your model and allows you to perform operations directly against them. While this is really awesome, I'm struggle to figure out how I can include these objects in state and still follow the "never modify state directly" tenant of react.
If I had a component that updated the account name, would it be acceptable to do something like this?
interface IAppState {
account: Account
}
class App extends React.Component<{}, IAppState> {
constructor(props) {
super(props);
this.state = {
account: new Account()
}
}
//set the new name and update the external service
public updateAccount = (newName: string)=>{
account.name = newName; //REDFLAG!!!
acc.update().then(()=>{
this.setState({ account: this.state.account })
})
}
//retrieve our account object from external service
public componentDidMount() {
const qParams = queryString.parse(window.location.search);
Account.get(qParams.externalId).then((acc)=>{
this.setState({account: acc})
})
}
render() {
return <NameEditor handleClick={this.updateAccount} account={a} />
}
}
I guess I could avoid mutating state by initiating a blank ORM object, copying the properties over, sending the update and then setting state, but this seems like a major pain.. Especially since these ORM objects can contain child ORM objects that I'd like to be able to modify as well.
Is the way I'm mutating state above "dangerous" or "bad form"???
Update
Did some reading and it definitely seems like this is probably bad form and might be navigated elegantly using the react/addons... However, what if the ORM call has a side effect on the object? For example, calling insert sets the objects external id field.
public updateAccount = (newName: string)=>{
//account.name = newName; //REDFLAG!!!
// You can use the below code to update name after it is updated
// on server.
// This will work because the object being passed here
// will be merged with Component state.
acc.update().then(()=>{
this.setState({account: {name : newName}})
})
}
Directly modifying state is not recommended as react will not come to know of the change and it will not cause a rerender.
All the diffing happens on Virtual DOM and react only updates the change attributes to Browser DOM. You can read more about the react diffing algorithm here.
React recommends to use immutable objects for setting state. you can either use Object.assign or immutable.js for this purpose which will make our life easier.
If you mutate your state object it will affect the the performance of your react component.
You can refer the below link for more information.
https://facebook.github.io/react/docs/optimizing-performance.html#examples

react meteor data container doesn't update child when props change

I have been struggling with this issue for quite some time and have failed to find any answers.
I use react-meteor-data to manage my data with react in my meteor application. It is working fine when dealing with data for mongo but I can't make it reactive with props.
Here in App.js, I call my container which I want to be reactive and rerender when the state of App change.
<MyContainer someState={this.state.MyState} />
In MyContainer.js I have a createContainer from react-meteor-data
export default createContainer(params => {
Meteor.subscribe('someCollection');
return {
someCollection: SomeCollection.find({}).fetch(),
stateFromParent: params.someState
};
}, MyContainer);
This worked fine when rendering the component for the first time, MyContainer correctly get MyState.
The thing is, when the MyState from App change, I can see in Chrome Dev React tool that it is indeed updated for the createContainer( ReactMeteorDataComponent has a prop with the right updated state) but the createContainer function is not run, thus the props do not update for MyContainer.
So the props are updated from ReactMeteorDataComponent but not for MyContainer who keeps indefinitely the data. It's like createContainer doesn't consider the update of its prop has a change and thus doesn't run its function.
I really think I'm missing something since that seems pretty basic stuff, thank you for your help.
The OP did not mention how the state was changed, so the original example is incomplete. Therefore, I will try to explain the gist of how the container creation works, in hope that understanding it will be useful.
How does it work?
It uses meteor's Tracker to auto-update the wrapped component when its computation is invalidated (i.e, when one of the reactive data sources, such as reactive variables, subscription handles or fetched MiniMongo cursors, has a new value). To learn more about Tracker, consult the Tracker manual. This is an in-depth resource, and is not necessary to understand how the basics work.
It does so in a way that is different from the way you normally approach reactivity tracking in Meteor, since it also needs to re-run the computation whenever the container's props are changed.
The source code is not very long or complex and can be found on GitHub (currently here).
Tracker.autorun((c) => {
if (c.firstRun) {
//...
data = component.getMeteorData();
} else {
// Stop this computation instead of using the re-run.
// We use a brand-new autorun for each call to getMeteorData
// to capture dependencies on any reactive data sources that
// are accessed. The reason we can't use a single autorun
// for the lifetime of the component is that Tracker only
// re-runs autoruns at flush time, while we need to be able to
// re-call getMeteorData synchronously whenever we want, e.g.
// from componentWillUpdate.
c.stop();
// Calling forceUpdate() triggers componentWillUpdate which
// recalculates getMeteorData() and re-renders the component.
component.forceUpdate();
}
})
Whenever the computation is invalidated (and therefore rerun), it stops the computation and forces a re-render of the container, which will re-create a new computation and have the updated data.
The high-level container functionality is here (some parts were removed for brevity):
export const ReactMeteorData = {
componentWillMount() {
this.data = {};
this._meteorDataManager = new MeteorDataManager(this); // (1)
const newData = this._meteorDataManager.calculateData(); // (2)
this._meteorDataManager.updateData(newData); // (3)
},
componentWillUpdate(nextProps, nextState) {
// backup current state and props, assign next ones to components
let newData = this._meteorDataManager.calculateData(); // (2)
this._meteorDataManager.updateData(newData); // (3)
// restore backed up data
},
componentWillUnmount() {
this._meteorDataManager.dispose(); // (4)
},
};
The main points are:
- Before being mounted, a new data manager is created (1). It is in charge of running the computation and populating this.data according to data changes.
- At first and whenever the component should update, the computation is run (2) and the data is updated (3). The update happens whenever the component receives new state or props (in this type of container, it should only be props), and, as we saw earlier, also when the Tracker computation is invalidated, due to the call to component.forceUpdate().
The wrapped component receives the parent's props, as well as the Tracker computation's data as props:
return <WrappedComponent {...this.props} {...this.data} />;
Any more points as to how it should be used?
The react-meteor-data has a short section in the meteor guide.
Generally, the simple example in the guide (as well as the OP's example) should work just fine, as long as the state is set appropriately, using setState() (see the "how does it work?" section above).
Also, there is no need to re-map the container state to props sent to the child, as they are passed along (unless there is a very good reason for doing so).
Do consider the point in the preventing re-renders section if you encounter any performance issues.
From the guide:
export default ListPageContainer = withTracker(({ id }) => {
const handle = Meteor.subscribe('todos.inList', id);
const loading = !handle.ready();
const list = Lists.findOne(id);
const listExists = !loading && !!list;
return {
loading,
list,
listExists,
todos: listExists ? list.todos().fetch() : [],
};
})(ListPage);
in this example, note that the container expects an id prop, and it will also be made available to the wrapped component, as well as loading, list, etc (which come from the container's computation in the example).

Resources