React : Best practice to handle complex objects passed as props - javascript

Lets assume I have a React component (functional component) and i am passing multiple props. One of these props is a nested object like...
Example nested object
const user = {
id: 101,
email: 'jack#dev.com',
personalInfo: {
name: 'Jack',
address: {
line1: 'westwish st',
line2: 'washmasher',
city: 'wallas',
state: 'WX'
}
}
}
For simplicity I want to pass the whole object, but instead of accessing it via user.personalInfo.adress.line1 I would like to save this to variable.
I am passing it to the following component...
Example React Component
const ExampleComponent = (props) => {
// Example 1
const [state1, setState1] = useState();
useEffect(() => {
setState1(user.personalInfo.adress.line1);
})
// Example 2
const state1Var = user.personalInfo.adress.line1
}
I know that saving props to states is a bad practice. (= Example1) So whats the smartest way to do that?
Saving it to a class variable? (= Example2)

You do not need a state for this. You can simply destructure the object on render of components.
Note: using useEffect without dependency variable, will retrigger the callback function on each render. It will be a memory issue useEffect(() => {},[]).
const user = {
id: 101,
email: "jack#dev.com",
personalInfo: {
name: "Jack",
address: {
line1: "westwish st",
line2: "washmasher",
city: "wallas",
state: "WX",
},
},
};
const ExampleComponent = ({ user = {} }) => {
const { personalInfo = {} } = user;
const { name = "somedefault name", address = {} } = personalInfo;
return (
<div>
{name}
<br />
{address.line1}
<br />
{address.line2}
</div>
);
};

There's nothing wrong with passing complex objects as props. Why do you need to save it to state? Just destructure the object in the child component (assuming it is not possibly null or undefined). React functional components are just functions, you don't need to save something to state in order to use it. Since you're passing it as a prop from a parent component, presumably the parent component keeps track of its state; when the parent state updates, the prop will change and your child component will rerender (or, in other words, your child component's function will be called with the new prop value(s)).
const ExampleComponent = (props) => {
const {personalInfo} = props.user;
/* ... */
}

Related

How to indicate to TS that value in store has value even though it's optional?

I have the following case:
I have a standard store with optional items in it.
I also have a tree of elements which rely on that store. I also select {account} in multiple components.
For business logic, I had to check at the very top if account is set. If it is not, I don't render the components which rely on it.
How can I tell TS that even though the value is optional in store I'm 100% sure it is NOT undefined?
Example code:
// store
interface Account {
id: number;
name: string;
}
export interface AppState {
account?: Account;
}
const initialState: AppState = {};
const accountSlice = createSlice({
name: "account",
initialState,
reducers: {
setAccount(state: AppState, action: PayloadAction<Account | undefined>) {
state.account = action.payload;
}
}
});
// component
const GrandChild = () => {
const { account } = useSelector((state: RootState) => state, shallowEqual);
return <>{account.name}</>;
};
const Child = () => {
const { account } = useSelector((state: RootState) => state, shallowEqual);
return account ? <GrandChild /> : <>account not set</>;
};
export default function App() {
const dispatch = useDispatch();
useEffect(() => {
// dispatch(setAccount({ id: 0, name: "john" }));
});
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<Child />
</div>
);
}
Codesandbox:
https://codesandbox.io/s/required-reducer-ysts49?file=/src/App.tsx
I know I can do this:
const { account } = useSelector((state: RootState) => state, shallowEqual) as Account;
but this seems very hacky. Is there a better way?
Well you know that Grandchild wont be rendered if account is undefined. so why not pass account as a property to the Grandchild component. This way you could define the property as never being undefined. And since you only render Grandchild after you checked that account isn't undefined you should be able to pass account as the property to the component (and since you defined the property as not being undefined TS will not object to account.name in your Grandchild component.
I don't know redux however - I have never used it and don't know anything about it, so I don't know if this answer is compatible with that or if redux will cause some issues I couldn't forsee.
I've written a little bit of code of how this could look (but as I already said, I don't know how to use redux, so you'll probably have to take my idea and write it so everything works) - so my code example is probably more of a visualization of what I mean than a solution.
const GrandChild = (account: Account) => {
return <>{account.name}</>;
};
const Child = () => {
const { account } = useSelector((state: RootState) => state, shallowEqual);
return account ? <GrandChild account={account} /> : <>account not set</>;
};

How to use useEffect hook with the dependency list as a specific field in an array of objects?

Let's say I have an array like this:
[
{
country: '',
'city/province': '',
street: ''
},
{
country: '',
'city/province': '',
street: ''
}
]
How do I have the useEffect() hook run every time the value of the 'country' field in any item inside the array changes?
Normally you wouldn't want to do that, but just to answer your question, it can be done, so let me propose the following assuming your list is called items:
useEffect(() => {
}, [...items.map(v => v.country)])
What the above code does is to spread all items (with its country property) into the useEffect dependency array.
The reason why this can be adhoc is mainly because React doesn't like to have a variable length of dependency. In the source code, when the length changes, it only appreciates the element change from the existing elements. So you might run into problem if you switch from 1 elements to 2 elements.
However if you have fixed number of elements, this should do what you wanted. Keep in mind the items has to be an array at all time.
NOTE: to accommodate the length issue, maybe we can add an additional variable length to the dependency array :)
}, [items.length, ...items.map(v => v.country)])
As i mentioned, most of time, you should avoid doing this, instead try to change the entire items every time when an item changes. And let the Item display to optimize, such as React.memo.
I don't think you can specifically tackle it in the dependency array, however, you can do your check inside the useEffect to have the same overall outcome.
Basically, the dependency array is passed the full data state, which will trigger the effect every change, then you do a further check if the sub property has changed.
I'm leverage lodash for brevity, but you can run any function to determine if the data has changed.
Codepen: https://codepen.io/chrisk7777/pen/mdMvpvo?editors=0010
const { useState, useEffect, useRef } = React;
const { render } = ReactDOM;
const { isEqual, map } = _;
const App = () => {
const [data, setData] = useState([
{
country: "",
"city/province": "",
street: ""
},
{
country: "",
"city/province": "",
street: ""
}
]);
const prevData = useRef(data);
// hacky updates just to demonstrate the change
// change country - should trigger useEffect
const update1 = () => {
setData((s) => [s[0], { ...s[1], country: s[1].country + "a" }]);
};
// change street - should not trigger useEffect
const update2 = () => {
setData((s) => [s[0], { ...s[1], street: s[1].street + "a" }]);
};
useEffect(() => {
if (!isEqual(map(prevData.current, "country"), map(data, "country"))) {
console.log("country changed");
}
prevData.current = data;
}, [data]);
return (
<div>
<button onClick={update1}>change country - trigger effect</button>
<br />
<button onClick={update2}>change street - do not trigger effect</button>
</div>
);
};
render(<App />, document.getElementById("app"));
Just map the countries into the effect dependency array.
const countries = data.map((x) => x.country);
useEffect(() => {
console.log(countries);
}, countries);

useEffect tries to read property of object before the state was updated in React

I am cascading data to my components with createContext() in React. In this I have an API call, where I'm fetching two different arrays of objects, mapping over them, and creating a final set of data like:
const [organizationData, setOrganizationData] = useState([
{
id: "",
orgName: "",
street: "",
city: ""
}
])
const organizationFunction = async () => {
try {
const data = await axios
.get(
API_URL + `organizations/${someUniqueId}`
)
.then(res => {
const masterData = res.data.master
const contactData = res.data.contact
const combinedData = masterData.map((item, i) => Object.assign({}, item, contactData[i]))
setOrganizationData(combinedData)
})
setOrgLoading(true)
} catch (e) {
alert(e)
}
}
Then I send the data to children
<AuthContext.Provider organizationData={organizationData}>
{children}
</AuthContext.Provider>
Now, when I try to access this data from one of the children, specifically to populate the inputs of a form with the data from the database, with the help of useEffect when the component is mount, it crashes because in the beginning organizationData is undefined, just the empty object I have defined upfront, so it tries to read organizationData.orgName and breaks with Cannot read property 'orgName' of undefined.
If I console.log(organizationData) in the Context component, the output is
[{…}]
0: {id: "", orgName: "", street: "", city: ""}
(1) [{…}]
0: {id: 12, orgName: "MyOrganization", street: "Skalitzerstr. 31", city: "Berlin"}
So the first output is empty and then the state updates.
I have tried to short circuit when adding data to my form in my child component but that is not working, it still crashes with Cannot read property 'orgName' of undefined.
useEffect(() => {
setFillingData({
...fillingData,
orgName: organizationData.orgName || "",
...
})
}
Any idea what exactly is wrong here, and how can I avoid exporting organizationData before the state was updated and the data/object created?
Thanks!
You can add organizationData in dependency array of useEffect
useEffect(() => {
if(organizationData){
setFillingData({
...fillingData,
orgName: organizationData.orgName || "",
...
})
}
}, [organizationData])

Having trouble incrementing the age when the button is clicked

New to react and I am having a hard time trying to increment the age of the person when the button is clicked.
EventComp.js code:
import React, { Component } from 'react';
class EventComp extends Component{
constructor(props) {
super(props);
this.state = {
clicks: 0,
show: true,
};
}
IncrementItem = () => {
this.setState({clicks: this.state.clicks + 1});
}
render(){
return(
<div>
<div class = "person">
<h1 class="person_h1">
{this.props.LastName},
{this.props.FirstName}</h1>
<p>Age: {this.props.Age}</p>
</div>
<button onClick = {this.IncrementItem}>Celebrate</button>
</div>
)
}
}
export default EventComp;
App.js code:
import React from 'react';
import EventComp from './components/EventComp';
import './App.css';
function App() {
return (
<div class ="main">
<div>
<EventComp FirstName="Jane "
LastName=" Doe " Age = {45}/>
</div>
<div>
<EventComp FirstName = "Joe " LastName = " Doe " Age={88}/>
</div>
<div>
</div>
</div>
);
}
export default App;
I'm just confused on how it's supposed to work. There are no errors, but when the button is clicked nothing happens.
…having a hard time trying to increment the age…
A common way to do this sort of thing is to have a parent component and a child component (App and EventComp) with responsibilities separated such that the parent manages the state (keeps track of the age) and the child (EventComp) is a "dumb" component that just renders whatever information it's given.
You're already doing most of this by passing the "person" information as props:
<EventComp FirstName="Joe" LastName="Doe" Age={88} /> // 👍
If you keep track of the age as state in App, then changing "age" will re-render, passing the updated age to EventComp:
function App () {
// create a state variable and a setter function for updating its value
const [age, setAge] = React.useState(88);
// pass age as a prop:
return (
<EventComp FirstName="Joe" LastName="Doe" Age={age} />
);
}
How does age get updated? By calling setAge. Here's the same example from above with the addition of a button that increments age:
function App () {
const [age, setAge] = React.useState(88);
return (
<div>
<button onClick={() => setAge(age + 1)}>Increment Age</button>
<EventComp FirstName="Joe" LastName="Doe" Age={age} />
</div>
);
}
Each click will trigger a re-render showing the new age.
But you want the button in EventComp not in App. How do we connect the button in EventComp to the state in App? By passing the update function as a prop:
function App () {
const [age, setAge] = React.useState(88);
return (
<EventComp
FirstName="Joe"
LastName="Doe"
Age={age}
onIncrementAge={() => setAge(age + 1)} // <= pass the age incrementing function as a prop
/>
);
}
function EventComp (props) {
return (
<div>
<div class = "person">
<h1 class="person_h1">
{props.LastName},
{props.FirstName}</h1>
<p>Age: {props.Age}</p>
</div>
<button onClick={props.onIncrementAge}> // <= onClick calls the function provided via props
Celebrate
</button>
</div>
)
}
Using this pattern, there's a single source of truth about the value of age, and EventComp doesn't need to know or care where it comes from.
Extending this to handle more than one person:
The examples above demonstrate the fundamental pattern but don't address the need to handle more than one person. Suppose you have a list of people:
const people = [
{
firstName: 'Jane',
lastName: 'Doe',
age: 45
},
{
firstName: 'Joe',
lastName: 'Doe',
age: 88
},
]
You can iterate over this list, rendering a component for each person:
function App () {
// initial state with 2 people
const [people, setPeople] = React.useState([
{ firstName: 'Jane', lastName: 'Doe', age: 45 },
{ firstName: 'Joe', lastName: 'Doe', age: 88 },
]);
return (
people.map((person) => ( // render an EventComp for each person
<EventComp
FirstName={person.firstName}
LastName={person.lastName}
age={person.age}
key={person.firstName} // key is a react thing. ignore for now.
/>
));
)
}
Updating a person's age becomes slightly more complicated, but it's still fairly straightforward. We need a function that does the following:
Find the person by index (their position in the list)
Update their age
Update state with the new info
const incrementAge = index => {
const person = people[index];
person.age += 1;
setPeople([...people]);
}
Pass this new function to EventComp and you're done.
function App () {
// initial state with 2 people
const [people, setPeople] = React.useState([
{ firstName: 'Jane', lastName: 'Doe', age: 45 },
{ firstName: 'Joe', lastName: 'Doe', age: 88 },
]);
const incrementAge = index => {
const person = people[index];
person.age += 1;
setPeople([...people]); // spread to a new array so react recognizes it as new
}
return (
people.map((person, index) => ( // render an EventComp for each person
<EventComp
FirstName={person.firstName}
LastName={person.lastName}
age={person.age}
onIncrementAge={() => incrementAge(index)} // <= indicate which person via index
key={person.firstName} // key is a react thing. ignore for now.
/>
));
)
}
Notice that we've added the index argument to the map call:
people.map((person, index) => {
And we're using it when we invoke the click handler:
onIncrementAge={() => incrementAge(index)}
Voila.
You could also pass the person as a single prop:
<EventComp
person={person}
onIncrementAge={() => incrementAge(index)}
/>
// use props.person.FirstName, etc. in EventComp
Hope this helps.
You're not updating age in your IncrementItem function. Instead, you're updating clicks. The reason you're not seeing anything happening is because clicks is not rendered anywhere. If you add a log statement and log clicked, you will see it being updated.
But if you are interested in incrementing age, you first need to add state to age:
this.state = {
clicks: 0,
show: true,
age: this.props.Age
};
As a note, in the future, props are usually lowercase and I would suggest having that prop be passed in as "age" instead.
The second thing you should do in your increment function is to update the age rather than clicks. If you add a console statement, you will see that the function is actually being invoked. You will also notice that age is being incremented rather than clicks because we are interested in that piece of state. The function should also be defined where the state is defined to retain context.
Note: functions in react are lowercased, components are capitalized. Functions also tend to use a "handle" naming convention, in this case, the function should be named something like "handleIncrementAge"
IncrementItem = () => {
this.setState({clicks: this.state.clicks + 1,
age: this.state.age + 1
});
}
Lastly, in your render, because it seems like you are interested in updating state, you want to display that piece of state.
<p>Increment Age: {this.state.age}</p>
*class attributes should be changed to className because class is a reserved keyword in React. Not doing so will result in your styles not being rendered.
I have attached a CodeSandbox with your code and the recommended changes: Code Sandbox
Updating State with React Class Components

How would I pass a getParam param to another screen using react-navigation in a stateless component to fire up a graphql mutation?

Following the docs: react-navigation params, I'd basically do something like this (parent):
this.props.navigation.navigate('Details', {
itemId: 86,
otherParam: 'anything you want here',
});
And then passing to the child
class DetailsScreen extends React.Component {
render() {
const { navigation } = this.props;
const itemId = navigation.getParam('itemId', 'NO-ID');
const otherParam = navigation.getParam('otherParam', 'some default value');
return ( (..omited for brevity)
EDIT:
My situation is that in one screen/container we fire up a graphql mutation that takes a phoneNumber and retrieves a code to user.
Onto the next screen I need to take this phoneNumber that the user has just inserted to fire another mutation that takes phoneNumber and code to make the authentication. I don't want to use states to do that, since there's api available on react-navigation.
Parent:
signInUser(phoneNumber: string) {
const { navigation, sendCode } = this.props
sendCode({ variables: { phoneNumber } }) // graphql mutation
navigation.navigate('Authenticate', { phoneNumber }) // passing
// phoneNumber as param
}
Child:
export const AuthenticationCodeModal = (props: NavigationScreenProps) => {
const {navigation} = props
const phoneNumber = navigation.getParam(phoneNumber)
// console.log(phoneNumber)
// I need to get phoneNumber here and pass it as props to
// <AuthenticationFormContainer> which will fire up auth mutation
return(
<View style={styles.container} >
<AuthenticationFormContainer/>
</View>
)
}
It better and interesting if you use redux for task like this. Redux has a global state which can be assessed by any component
Turns out it was a simple syntax error of the value that getParam receives, doing this fixed my problem:
navigation.navigate('Authenticate', { phoneNumber: {phoneNumber: phoneNumber} })

Categories

Resources