How to pass props of a nested object to a component? - reactjs

I am having a little hard time figuring out this issue and would appreciate if someone clear this concept to me. With the following code, I can parse props to another component i.e. UserItem and it displays the username and email. I also want to display "firstname" along with other details. I can easily destructure others like: const {username, email} = this.state.user but when I add const {name: fisrtname, username, email}, then it doesn't work. But if I put this.state.user.name.firstname in the UserItem component, then I can see the firstname in the browser. So how I will destructure the nested object and/or parse the props of nested object to other component? Thanks in advance.
users: [
{
username: "johndoe",
email: "jdoe#gmail.com",
name: {
firstname: "John",
id: "2",
},
id: "1",
},
],
};
render() {
return (
<div>
{this.state.users.map((user) => (
<UserItem key={user.id} user={user} />
))}
</div>
);
}
}
**This is UserItem component**
class UserItem extends Component {
render() {
const { name: firstname, email, username, phone } = this.props.user;
// console.log(this.props.user);
return (
<div>
<h3>{this.props.user.name.firstname}</h3>
<h3>{email}</h3>
<h3>{username}</h3>
<h3>{phone}</h3>
</div>
);
}
}

I am just extending and describing the #Alex Yepes's Answer.
Look at your code
{
username: "johndoe",
email: "jdoe#gmail.com",
name: { // here the name is also an object.
firstname: "John",
id: "2",
},
id: "1",
},
Since 'name' is also an object you need to destructure that one too.
For example:
On your first destructure, you can only access directly the property of that object
const {username, email, name, id} = user;
console.log(username); //result: 'johndoe'
console.log(name); //result: {firstname: 'John', id: 2}
Here you can see that username's value is a string but the value of 'name' is an object that's why you need to destructure this one too. Other wise you have to use object or bracket notation like:
console.log(name.firstname); // result: 'John'
//or
console.log(name[firstname]) // result: 'John'
But if you destructure like this one:
const { name: {firstname}, email, username, phone } = this.props.user;
you can access 'name' directly because the 'name' object is not an object anymore.

This:
const { name: firstname, email, username, phone } = this.props.user;
needs to be changed to this:
const { name: {firstname}, email, username, phone } = this.props.user;
That way you can access the nested property.

you need the constructor in the object above the render.
class UserItem extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {

Related

Migration to Mobx 6: functional components aren't working with decorated observables

I faced with problem while migrating from Mobx 4 to Mobx 6.
I have a functional component but after updating Mobx it stopped working. Looks like store doesn't works. Component react on changes inside observable variable by reaction feature but changes aren't re-rendering. I made everything that was provided in migration guide but component's store doesn't working.
At some reason if I change functional component to class component everything starts working. But I really can't understand the reason why such happens and can't find any explanation of such behaviour.
Case looks like example bellow. Experimental decorators are enabled and any other stuff that was provided in Migration guide as well. So what is the reason of such behaviour and how can I implement correct logic in functional component?
interface User {
name: string;
age: number;
info: {
phone: string;
email: string;
};
}
const usersData: User[] = [
{
name: "Steve",
age: 29,
info: {
phone: "+79011054333",
email: "steve1991#gmail.com",
},
},
{
name: "George",
age: 34,
info: {
phone: "+79283030322",
email: "george_the_best_777#gmail.com",
},
},
{
name: "Roger",
age: 17,
info: {
phone: "+79034451202",
email: "rodge_pirat_yohoho#gmail.com",
},
},
{
name: "Maria",
age: 22,
info: {
phone: "+79020114849",
email: "bunnyrabbit013#gmail.com",
},
},
];
const getUsers = () => {
return new Promise<User[]>((resolve) => {
setTimeout(() => {
resolve(usersData);
}, 2000);
});
};
class Store {
#observable users: User[] = [];
constructor() {
makeObservable(this);
}
async init() {
const users = await getUsers();
this.setUsers(users);
}
#action setUsers(users: User[]) {
this.users = users;
}
#action increaseUserAge(userIndex: number) {
const users = this.users.map((u, k) => {
if (k === userIndex) {
u.age += 1;
}
return u;
});
this.setUsers(users);
}
#computed get usersCount(): number {
return this.users.length;
}
}
const store = new Store();
const UserList = observer(() => {
React.useEffect(() => {
store.init();
}, []);
const addOneUser = () => {
const user = {
name: "Jesica",
age: 18,
info: {
phone: "+79886492224",
email: "jes3331#gmail.com",
},
};
store.setUsers([...store.users, user]);
};
return (
<div className="App">
<h4>Users: {store.usersCount}</h4>
{store.users.length ? (
<>
<ul>
{store.users.map((user, key) => (
<li key={key}>
Name: {user.name}, Age: {user.age}, Info:
<div>
Phone: {user.info.phone}, Email: {user.info.email}
</div>
<button onClick={() => store.increaseUserAge(key)}>
Increase Age
</button>
</li>
))}
</ul>
<button onClick={addOneUser} disabled={store.usersCount >= 5}>
Add one user
</button>
</>
) : (
<p>Fetching users...</p>
)}
</div>
);
});
function App() {
return <UserList />;
}
export default App;
I've made Codesandbox example with your code (although removed types), it works fine.
Check tsconfig.json there, maybe you forgot to enable some of the options?
Or check what versions of mobx and mobx-react are you using?
And just a small nitpick on how you use your increaseUserAge action, it can be as simple as that:
#action increaseUserAge(user) {
user.age += 1;
}
And in the jsx you just pass the whole user there:
<button onClick={() => store.increaseUserAge(user)}>
Increase Age
</button>

Filter Search in React JS

Im having trouble filtering data based on my input search... my goal is to search for 'M' and to see a list of all payload data containing 'M' and as I get closer to the exact string 'Matt' only show the payload data for 'Matt' and bacisally be able to search by first_name, last_name or number. Thank you in advance, ANY feedback is appreciated! Ive been stuck on this >_<
Im using a Custom component library and trying to do a basic search & filter, the issue is not with the custom library but with the filter function. It doesnt seem to retrieve the v.first_name value. Im also open to any other sorts of filter libraries / approaches.
I typed in the letter 'M' into the searchbox UI component and got the following outputs for each console log statement
import React, { Component } from 'react';
import { IS_FETCHING_DBUSERS, FETCH_DBUSERS_SUCCESS } from '../../actions/keys';
import { users } from '../../actions/URI';
import { fetchComponent } from '../../actions/index';
import { connect } from 'react-redux';
import _ from 'lodash';
import {
LumosTheme,
Grid, Form, Icon, Container, Loader
} from '#CustomLibrary/core';
class SearchBar extends Component {
state = {
responseData: " ",
handle: true,
query: "",
filterValues: []
};
searchString = this.state.query;
responseData = this.props.Users.data;
componentDidMount() {
this.props.fetchComponent([IS_FETCHING_DBUSERS, FETCH_DBUSERS_SUCCESS], users).then(() => {
return this.setState({
handle: false
})
})
}
handleInputChange = (value) => {
console.log("In HandleInputFunction, Value= ", value) // Output of value is 'M'
this.setState({query:value}, () => {
console.log("In HandleInputFunction, query= ", this.state.query) // Output of query is 'M'
this.filterArray();
}
)
}
filterArray = () => {
console.log('In Filter fxn')
let searchString = this.state.query;
let responseData = this.props.Users.data;
console.log('This is the responseData in Filter: ', responseData); // output of responseData is '(6)[{...},{...},{...},{...},{...},{...}]'
console.log('This is my filterValues in Filter: ', this.state.filterValues); //output of filterValues is '[]'
console.log('This is my searchString in Filter: ', searchString) //output of searchString is 'M'
if(searchString.length > 0){
const filterData = _.filter(this.state.responseData, (v) => v.first_name === searchString);
console.log('This is my filterData in loop: ',filterData) //output of filterData is '[]'
this.setState({
filterValues : filterData
})
console.log('This is my filterValues in loop: ', this.state.filterValues) //output of filterValues is '[]'
}
}
// for now this drop down 'searchByOptions' is hard coded and just here for UI visual purposes, what I want to do later is depending on the option the user choses I pass the correct payload.
searchByOptions = [
{ label: 'Name or number', value: 'NAME/number' },
{ label: 'Distribution List', value: 'DL' },
{ label: 'Database Schema or Table', value: 'DB' },
{ label: 'Role', value: 'Role' }
];
render() {
if (this.state.handle) {
return <Loader />
}
else {
return (
<LumosTheme>
<Container width='1379px'>
</Container>
<Container width='1379px'>
<Grid paddingTop='10px'>
<Form.Item width='380px'>
<Form.Dropdown
options={this.searchByOptions}
defaultOption={this.searchByOptions[0]}
/>
</Form.Item>
</Grid>
<Grid flexWrap="wrap" width='1000px'>
< Form.SearchBox placeholder='Search' icon={<Icon.SearchRounded />}
userSelectOnClick
openOnClick
onSearch={this.handleInputChange}
value={this.state.query}
>
<Form.SearchList >
{this.state.responseData ?
this.state.filterValues.map(item => (
<Form.SearchOption
value={item.first_name}
/>
)):'null'}
</Form.SearchList>
</ Form.SearchBox>
</Grid>
</Container>
</LumosTheme>
)
}
}
}
const mapStateToProps = state => {
return {
Users: state.users
}
}
export default connect(mapStateToProps, { fetchComponent })(SearchBar);
my payload is being fetched correctly and looks like so
0: {id:1, first_name: "Matt", last_name: "Jones", number:"123",}
1: {id:2, first_name: "Alex", last_name: "Lee", number:"675",}
2: {id:3, first_name: "Adam", last_name: "Khan", number:"733",}
3: {id:4, first_name: "Sue", last_name: "Kid", number:"248",}
4: {id:5, first_name: "Jade", last_name: "Smith", number:"907",}
5: {id:6, first_name: "Luca", last_name: "James", number:"125",}
It looks like you are doing an exact match with that filter condition.
You can use _.filter(this.state.responseData, (v) => v.first_name.includes(searchString));.
Pro tip: once you go lodash, you never go back... That didn't rhyme, but you get the point.

Want to populate the input values based on the click of an object inside map(): React+Typescript

I am maintaining an array of objects which is stored in a state object. Basically I am pushing each object to this array whenever I click on Add button .This stores this object in array.
Also I am iterating this array of objects to display down the page.
Right now I am trying to fill the input fields based on the object that I have clicked. I am unable to do it. Basically, the object that I have clicked should populate the input fields and then I should be able to edit it
Help would be appreciated
The structure of array of objects:
users= [
{"name":"xxx","email":"yyy","phone":"656"},
{"name":"yyy","email":"xxx","phone":"55"}
];
Component Code
import * as React from 'react';
interface IState{
users : Account[];
user: Account
}
interface Account{
name: string;
email: string;
phone: string
}
export default class App extends React.Component<{},IState> {
constructor(props:any){
super(props);
this.state= {
users: [],
user: {
name: '',
email: '',
phone: '',
}
}
}
removeAccount = (i:number) => {
let users = [...this.state.users];
users.splice(i,1);
this.setState({users},()=>{console.log('setting the data')});
}
handleChange = ( event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
user:{
...this.state.user,
[event.currentTarget.name]:event.currentTarget.value
}
})
}
onAdd = () => {
e.preventDefault();
this.setState({
users: [...this.state.users, this.state.user],
user: { name:'', email: '', phone: ''}
},()=>{console.log('adding')});
}
clearInputs = () => {
this.setState({user: { name:'', email: '', phone: ''}});
}
showDetails = (i:number) => { //I need to populate the input fields based on the index of the object clicked.
console.log(i);
}
render(){
const { name, email, phone } = this.state.user;
<React.Fragment>
<form onSubmit={this.onAdd}>
<input type="text" value={name} onChange={(e:any) => this.handleChange(e)} name={"name"} />
<input type="text" value={email} onChange={(e:any) => this.handleChange(e)} name={"email"} />
<input type="text" value={phone} onChange={(e:any) => this.handleChange(e)} name={"phone"} />
<button type="submit">Add</button>
</form>
<ul>
{this.state.users.map((row:any ,index: number) =>
<li key={index}>
<a onClick={()=> this.showDetails(index)}><span>{row.name}</span></a> // on click of this,i need to display the values corresponding to this object in the above input fields
<i className="close far fa-times" onClick={() =>this.removeAccount(index)}/>
</li>
)}
</ul>
</React.Fragment>
}
}
Based on logic of the code showDetails should look like
showDetails = (i:number) => {
this.setState ({user: this.state.users.splice(i,1)});
console.log(i);
}
Just set user to the selected element of users array. React will do update and calls render() with updated data.
Also utilizing splice will remove currently editing user from array. THis follow logic of the code. After edit Add should be clicked to add modified user back to array. This may be not convenient, so you may consider adding editingIndex to state and specify which user object currently editing. In such case you'll have to save index of selected object in editingIndex. In handleChange you should check if some user object editing now and modify data not only in user property of state but in corresponding users array element
interface IState{
users : Account[];
user: Account;
editingIndex: number | null;
}
// In constructor
constructor(props:any){
super(props);
this.state= {
users: [],
user: {
name: '',
email: '',
phone: '',
},
editingIndex: null
}
}
showDetails = (i:number) => {
this.setState ({user: this.state.users[i], editingIndex: i});
console.log(i);
}
handleChange = ( event: React.ChangeEvent<HTMLInputElement>) => {
let user = {...this.state.user,
[event.currentTarget.name]:event.currentTarget.value};
this.setState({user});
// If we currently editing existing item, update it in array
if (this.state.editingIndex !== null) {
let users = [...this.state.users];
users[this.state.editingIndex] = user;
this.setState({users});
}
}
removeAccount = (i:number) => {
let users = [...this.state.users];
// If we're going to delete existing item which we've been editing, set editingIndex to null, to specify that editing ends
if (this.state.editingIndex === i)
this.setState({user: {name: '', email: '', phone: ''}, editingIndex: null});
users.splice(i,1);
this.setState({users},()=>{console.log('setting the data')});
}
onAdd = () => {
e.preventDefault();
// If we NOT editing, but adding new editingIndex will be null so add user to users array. If we editing existing element it's no need to add it once again.
if (this.state.editingIndex === null)
this.setState({ users: [...this.state.users, this.state.user] });
this.setState ({ editingIndex: null,
user: { name:'', email: '', phone: ''}
},()=>{console.log('adding')});
}
// render will have no change

ADD_SONG not added to PlayList

In my app
there's a component that renders (play)lists
(I have 2 lists hardcoded )
I can Add a new list to the list of lists.
When you click on a list
the list of songs is displayed, and at the bottom of the list is
a button that, when you click it, displays a form with inputs (title,artist,album).
Before I fixed the adding list functionality, songs were added to the 'active' list
but now
the action is dispatched (ADD_SONG) and shows up with the right values in the (Redux)state but it renders the same type of element/component as the list and is not appened/added...
I'm not sure where to look
I hope someone can spot my faulty logic
AddSongForm
export default class AddSongForm extends React.PureComponent {
constructor() {
super();
this.state = {
clicked: false
};
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
this.setState({
clicked: !this.state.clicked
})
}
handleChange = (event) => {
const value = event.target.value
const name = event.target.name
// console.log(name, value)
// console.log(this.state);
this.setState({
[name]: value
})
}
handleSubmit = (event) => {
event.preventDefault()
console.log(this.state);
if (this.state.title && this.state.artist) {
this.props.addSong({
title: this.state.title,
artist: this.state.artist,
album: this.state.album
})
}
}
render() {
return (<div>
<button onClick={this.handleClick}><h2>New Song+</h2></button>
{this.state.clicked ?
<form onSubmit={this.handleSubmit}>
<label>
Song Title:
<input type="text" name="title" onChange={this.handleChange} />
</label>
<label>
<br/> Artist:
<input type="text" name="artist" onChange={this.handleChange} />
</label>
<label>
<br/> Album:
<input type="text" name="album" onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
: null}
</div>)
}
}
AddSongFormContainer
import AddSongForm from './AddSongForm'
import { connect } from 'react-redux'
class AddSongFormContainer extends React.PureComponent {
addSong = (song) => {
this.props.dispatch({
type: 'ADD_SONG',
payload: {
id: Math.ceil(Math.random()*10000),
...song
}
})
}
render() {
return <AddSongForm addSong={this.addSong} />
}
}
export default connect(null)(AddSongFormContainer)
Reducer with initial state
const initState = [
{
id: 1,
title: 'Play list 1',
data: [
{
id: 1,
title: 'DogHeart II',
artist: 'The Growlers',
album: 'Gilded Pleasures'
}, {
id: 2,
title: 'Beast of No nation',
artist: 'Fela Kuti',
album: 'Finding Fela'
}, {
id: 3,
title: 'Satellite of love',
artist: 'Lou Reed',
album: 'Transformer'
}
]
}, {
id: 2,
title: 'Play list 2',
data: [
{
id: 1,
title: 'Whatever happend to my Rock and Roll',
artist: 'BlackRebelMoterCycleClub',
album: 'B.R.M.C'
}, {
id: 2,
title: 'U Sexy Thing',
artist: 'Crocodiles',
album: 'CryBaby Demon/ U Sexy Thing'
}, {
id: 3,
title: 'Oh Cody',
artist: 'NoBunny',
album: 'Raw Romance'
}
]
}
]
const reducer = (state = initState, action = {}) => {
switch (action.type) {
case 'ADD_LIST':
return [
...state,
action.payload
]
case 'ADD_SONG':
return [
...state,
action.payload
]
default:
return state
}
}
export default reducer
PlayList Component mapping over al the songs in the list
export default class PlayList extends React.Component{
constructor() {
super()
this.state = {
clicked: false
}
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState({
clicked: !this.state.clicked
})
console.log(this.props.selectList.name)
}
render(){
// console.log(this.props);
// console.log(this.props.playLists[0].data);
return (<div>
<button onClick={this.handleClick}><h1>{this.props.playLists.title}</h1></button>
{this.state.clicked ?
<ul>
{ this.props.playLists.data.map(song =>
<li key={song.id} onClick={() => this.props.selectSong(song.id)}>
<b>{song.title}</b><br/>
By:
<br/>
<h3><b>{song.artist}</b></h3><br/>
Appears on:
<b>{song.album}</b><br/><br/>
</li>
) }
<AddSongFormContainer/>
</ul>
: null}
</div>)
}
}
Container for al the playlists in the initialstate(array)
class PlayListsContainer extends React.PureComponent {
selectSong = (id) => {
this.props.dispatch({
type: 'SELECT_SONG',
payload: id
})
}
selectSong(id) {
console.log('selected song:', id)
}
selectList = (id) => {
this.props.dispatch({
type: 'SELECT_LIST',
payload: id
})
}
selectList(id) {
console.log('selected song:', id)
}
render() {
const playlistsArray = this.props.playLists
// console.log(playlistsArray)
return (
playlistsArray.map((playlist) => <PlayList
playLists={playlist}
selectSong={this.selectSong}
selectList={this.selectList}
key={playlist.id}
/>)
)
}
}
const mapStateToProps = (state) => {
// console.log(state.playLists);
return {
playLists: state.playLists
}
}
export default connect(mapStateToProps)(PlayListsContainer)
With your comment describing your redux problems, and the screenshot of the Redux dev tools - the problem is clear now.
When you are adding a song, you are simply adding it to the top level of the store, without actually adding it to a play list.
It would be entirely possible to fix this as is. In your reducer, rather than adding the song like you do now, you need to add it specifically to a playlist. If you need a code example, I can provide one.
However, I encourage you to refactor your redux store - and follow the best practicing of having a normalized, flat state.
What this means is, you want to have two top-level objects for your redux store.
playlists
songs
Rather than including all of the data about a song in a playlist, you simply reference the id of the songs.
Your playlists would look like this:
playlists {
1: {
title: 'my playlist'
songs: [1,2,3]}
And the songs can stay the same.
Whenever you add a song to a playlist, you simply add the song, and then update the playlist with the new song id.
Another practice you can do, to make your code a bit cleaner is to use mapDispatchToProps rather than defining your redux action dispatches inline. Docs for that are here.
#
To fix the code as is, the main thing we need to do is pass along the playlistid that you want to add the song to. Otherwise, how else will we know where to put the song?
First, update your action in your addSongFormContainer to accept an additional argument, targetPlaylist (that the song will go into)
addSong = (song, targetPlaylist) => {
this.props.dispatch({
type: 'ADD_SONG',
payload: {
playlist: targetPlaylist
id: Math.ceil(Math.random()*10000),
...song
}
})
}
The usage of this action now requires you pass along a target playlist. For brevity, I am going to hardcode that the song is being added to playlist 1. I'll leave the exercise of passing the selected playlist down to the component up to you.
I cleaned up the handleSubmit to make it more clear, by moving the song into it's own variable as well.
handleSubmit = (event) => {
event.preventDefault()
console.log(this.state);
if (this.state.title && this.state.artist) {
let song = {
title: this.state.title,
artist: this.state.artist,
album: this.state.album
}
let selectedPlayList = 1 //Fix this later :)
this.props.addSong(song, selectedPlayList)
}
}
Now the last problem is the reducer.
case 'ADD_SONG':
const index = store.getState().findIndex(playlist => playlist.id ===
action.payload.playlist)
console.log(index) //This should be index 0, after looking up playlist id: 1
const updatedPlaylistSongs = this.state[index].data
updatedPlaylistSongs.push(action.playload.song)
return [
...state.slice(0, index), // All playlists before current
{
...state[index], //The targeted playlist.
...updatedPlaylistSongs //The updated songs
},
...state.slice(index + 1), //All playlists after current
]
I hope the reducer works for you, though it might need a bit of work - I am not used to writing reducers dealing with arrays. I typically have normalized data, which results in much easier modification. I highly encourage you to attempt to normalize your redux store. Stop using arrays, try using objects where the key is generated (use uuidv4 to make a unique & random key). This makes "selecting" what you want to edit/update significantly easier.
I hope this helps!

React - update object in state

I need to update an object in state, but only some of its elements. So if it has 7 elements, and my new object has 4, those 4 should replace the existing ones while the rest should be preserved.
Here is an example component which outputs the current object keys and values in the state. When you press the button, the object should get updated with new values on some of its properties. Right now it is overwriting the object with the 4 elements, so I need to modify it. See the handleClick method.
In my real project the object is inside redux state, but I guess the solution will be the same. I get the new properties from a form that is posted, so I have an object like the one below named "update".
import React, { Component } from 'react';
import Button from 'material-ui-next/Button';
import Menu, { MenuItem } from 'material-ui-next/Menu';
class UpdateObject extends Component {
constructor(props) {
super(props)
this.state = {
theObject : {
token: '478478478478',
firstName: 'Goofy',
lastName: 'Hello',
age: '14',
sex: 'female',
employed: true,
favoriteColor: 'Blue',
bio: 'details of bio',
}
};
this.handleClick = this.handleClick.bind(this);
}
handleClick(){
let update = {
age: '40',
lastName: 'Newname',
employed: true,
favoriteColor: 'Yellow',
}
let change = Object.assign({}, this.state.theObject);
change = update;
this.setState({ theObject: change });
}
render() {
const myObj = this.state.theObject;
return (
<div className="updateobjectwrapper bl">
<div> Here is the current object: <br />
<ul>
{ Object.entries(myObj).map(([ key,value ] ) => {
return (
<li key={key}>{key} : {value} </li>
)
})
}
</ul>
</div>
<Button
onClick={this.handleClick}
>
Update Object
</Button>
</div>
)
}
}
You can pass multiple arguments to Object.assign method and it will merge all of them.
handleClick(){
let update = {
age: '40',
lastName: 'Newname',
employed: true,
favoriteColor: 'Yellow',
}
let change = Object.assign({}, this.state.theObject, update);
this.setState({ theObject: change });
}

Resources