Array passed as prop gets accepted as a string - reactjs

Edit: Very stupid overlook on my time. Next time I'll read the documentation more slowly.
Passing an array as a prop, it gets accepted as a string with the same name.
For example if i'm passing usersList = ['Name1', 'Name2', 'Name3'] to a child, the child only has access to usersList as a string, not its content.
Tried doing const Table = ({usersList}) => {...}, const Table = (usersList)=>{...}
The parent that inits the array:
import React from "react";
import { connect } from "react-redux";
import Table from "../presentational/Table.jsx";
const mapStateToProps = state => {
return { users: state.users };
};
const test = ["1", "4", "5"];
const ConnectedPeople = ({ users }) => {
return (
<ul className="list-group list-group-flush">
{console.log(test)}
<Table usersList="{test}" />
</ul>
);
};
const People = connect(mapStateToProps)(ConnectedPeople);
export default People;
The child accepting it
import React from "react";
const Table = usersList => {
return (
<div>
<pre>{Object.keys(usersList)}</pre>
</div>
);
};
export default Table;
I would assume I can just do usersList.map(...), but it comes as an object with every character of the string usersList. so the Object.keys(usersList) renders usersList

The problem is in the way you are passing the prop, which is in the form of a string. Instead you need to write
<Table usersList={test} />
Once you make the above change, you can simply access the array elements by mapping over the props like
this.props.usersList.map(...)

problem:
<Table usersList="{test}" />
fix:
<Table usersList={test} />

Related

Why when you delete an element out of react array, the inner element you pass it to remains

In a react component I have an array of things. I iterate through that array display the name of the thing in a plain div, then pass each element to another component to display details.
What's happening: if I delete an element from anywhere except the bottom (last array element) the header that is displayed in the main element containing the array is correct (the one I clicked "delete" on disappeared), but the "body" (which is another component) remains. Instead, the inner component is acting as if I deleted the last element of the array and kind of "moves" up the array.
It's hard to describe in words. See example below. Delete the top element or one of the middle ones and see how the header for the section starts not matching the contents.
I'm trying to understand why this is happening.
(EDIT/NOTE: State IS needed in the child component because in real life it's a form and updates the object being passed in. I Just removed the updating here to make the example shorter and simpler)
Example code (delete the middle element of the array and see what happens):
https://codesandbox.io/s/confident-buck-dodvgu?file=/src/App.tsx
Main component:
import { useState, useEffect } from "react";
import InnerComponent from "./InnerComponent";
import Thing from "./Thing";
import "./styles.css";
export default function App() {
const [things, setThings] = useState<Thing[]>([]);
useEffect(() => resetThings(), []);
const resetThings = () => {
setThings([
{ name: "dog", num: 5 },
{ name: "cat", num: 7 },
{ name: "apple", num: 11 },
{ name: "book", num: 1}
]);
};
const onDeleteThing = (indexToDelete: number) => {
const newThings = [...things];
newThings.splice(indexToDelete, 1);
setThings(newThings);
};
return (
<div className="App">
{things.map((thing, index) => (
<div key={`${index}`} className="thing-container">
<h2>{thing.name}</h2>
<InnerComponent
thing={thing}
index={index}
onDelete={onDeleteThing}
/>
</div>
))}
<div>
<button onClick={resetThings}>Reset Things</button>
</div>
</div>
);
}
Inner component:
import { useEffect, useState } from "react";
import Thing from "./Thing";
interface InnerComponentParams {
thing: Thing;
index: number;
onDelete: (indexToDelete: number) => void;
}
export const InnerComponent: React.FC<InnerComponentParams> = ({
thing,
index,
onDelete
}) => {
const [name, setName] = useState(thing.name);
const [num, setNum] = useState(thing.num);
return (
<div>
<div>Name: {name}</div>
<div>Num: {num}</div>
<div>
<button onClick={(e) => onDelete(index)}>Delete Me</button>
</div>
</div>
);
};
export default InnerComponent;
You are creating unnecessary states in the child component, which is causing problems when React reconciles the rearranged Things. Because you aren't setting the state in the child component, leave it off entirely - instead, just reference the prop.
export const InnerComponent: React.FC<InnerComponentParams> = ({
thing,
index,
onDelete
}) => {
return (
<div>
<div>Name: {thing.name}</div>
<div>Num: {thing.num}</div>
<div>
<button onClick={(e) => onDelete(index)}>Delete Me</button>
</div>
</div>
);
};
The other reason this is happening is because your key is wrong here:
{things.map((thing, index) => (
<div key={`${index}`}
Here, you're telling React that when an element of index i is rendered, on future renders, when another element with the same i key is returned, that corresponds to the JSX element from the prior render - which is incorrect, because the indicies do not stay the same. Use a proper key instead, something unique to each object being iterated over - such as the name.
<div key={thing.name}
Using either of these approaches will fix the issue (but it'd be good to use both anyway).
This is also wrong. You're removing everything except the index.
const onDeleteThing = (indexToDelete: number) => {
const newThings = [...things];
newThings.splice(indexToDelete, 1);
setThings(newThings);
};
Use filter:
const onDeleteThing = (indexToDelete: number) => {
const newThings = [...things].filter(
(thing, index) => index !== indexToDelete
);
setThings(newThings);
};

Best way to access react-table instance from child to parent component [need your opinions]

Currently I'm doing this:
import * as React from "react";
import { useTable } from "react-table";
const Table = ({ onInit }) => {
const table = useTable({ data: [...], columns: [...] });
React.useEffect(() => {
onInit(table);
}, []);
return <table>...</table>
};
const TableParent = () => {
const [table, setTable] = React.useState();
return <div>
<Table onInit={table => setTable(table)} />
<div>
{/* Used table instance here */}
</div>
</div>
};
but not sure if this is fine or it is a bad pattern.
I'm a React beginner.
Please, I want to hear your opinions.
Thank you!
I do it like this, and it works well
Let's assume:
Parent component is App and
Child component is Table
From Parent component: You can pass a callback to table component
In table component: You call the same callback passing instance as a parameter using hooks.useInstance plugin point
I made it easy for you, Check the console log:
https://codesandbox.io/s/get-react-table-instance-in-parent-component-rx66s?file=/src/App.js

Side panel with components loading dynamically based on context

With react: how can I render a side panel component with different components depending on the type of action. To give a concrete example:
On the main page, I have a table with some rows. Every row may be of a different "type" (Row 1 may be of type A, Row 2 may be of type B.
When I click on a row that's type A, I would like to render components related to the type A row, and different components for type B row.
You can almost define the panel like a "widget" area, where I can load different components based on different context, or action I perform outside of that area.
Below is what I am trying to do.
This can be done by changing the data stored on state of the selected row type.
For example:
state = {
currently_selected_type : ""
}
In your render function, we can have something like this:
render(){
let SidePanelComponent = null;
if(this.state.currently_selected_type === "A"){
SidePanelComponent = (<div>A</div>);
}
else if(this.state.currently_selected_type === "B"){
SidePanelComponent = (<div>B</div>);
}
return (
<div>
<Table
onClick={
// this type will be passed from the inside Table when rows are clicked based on the row type
(type) => this.setState({currently_selected_type : type})
}
/>
{SidePanelComponent}
</div>
);
}
Inside the Table component you will be rendering the different types of rows, so based on what row is click just send the type to the passed onClick function.
The Table render function can be as follows, for example:
render(){
return (
...
<tr onClick={this.props.onClick("A")}>Row Type A</tr>
<tr onClick={this.props.onClick("B")}>Row Type B</tr>
...
);
}
Ok, I figured it out:
I created a context and a provider
import React, { useState } from 'react';
import PanelContext from './panelContext'
const PanelProvider = ({ children }) => {
const [content, setContent] = useState(undefined);
return (
<PanelContext.Provider
value={{
content: content,
setContent: setContent
}}>
{children}
</PanelContext.Provider>
);
};
export default PanelProvider;
Then I created a usePanel hook:
import PanelContext from "../contexts/panel/panelContext";
const usePanel = () => {
const { content, setContent } = useContext(PanelContext)
return {content, setContent};
}
export default usePanel;
Then in my Table Row component, I use it:
const { setContent } = usePanel();
...
...
const testCallback = (e) => console.log(e.target.value)
...
...
setContent(<Input value="wow!!!!" onChange={testCallback} />)
And finally, on the actual panel component itself:
import { usePanel } from "../../../hooks"
export default ({ }) => {
const { content } = usePanel();
return (
<aside className="contextualizer" id="contextualizer">
<PerfectScrollbar>
<div className="customizer-body">
{content}
</div>
</PerfectScrollbar>
</aside >
);
}
Works perfectly.

React Hook useEffect() run continuously although I pass the second params

I have problem with this code
If I pass the whole pagination object to the second parameters of useEffect() function, then fetchData() will call continuously. If I only pass pagination.current_page so It will call only one time, but when I set new pagination as you see in navigatePage() function, the useEffect() does not call to fetchData() although pagination has changed.
How to solve this. Thank you very much!
Besides I do not want the use useEffect() call when first time component mounted because the items is received from props (It is fetch by server, this is nextjs project).
import React, {useEffect, useState} from 'react';
import Filter from "../Filter/Filter";
import AdsListingItem from "../AdsListingItem/AdsListingItem";
import {Pagination} from "antd-mobile";
import styles from './AdsListing.module.css';
import axios from 'axios';
const locale = {
prevText: 'Trang trước',
nextText: 'Trang sau'
};
const AdsListing = ({items, meta}) => {
const [data, setData] = useState(items);
const [pagination, setPagination] = useState(meta);
const {last_page, current_page} = pagination;
const fetchData = async (params = {}) => {
axios.get('/ads', {...params})
.then(({data}) => {
setData(data.data);
setPagination(data.meta);
})
.catch(error => console.log(error))
};
useEffect( () => {
fetchData({page: pagination.current_page});
}, [pagination.current_page]);
const navigatePage = (pager) => {
const newPagination = pagination;
newPagination.current_page = pager;
setPagination(newPagination);
};
return (
<>
<Filter/>
<div className="row no-gutters">
<div className="col-md-8">
<div>
{data.map(item => (
<AdsListingItem key={item.id} item={item}/>
))}
</div>
<div className={styles.pagination__container}>
<Pagination onChange={navigatePage} total={last_page} current={current_page} locale={locale}/>
</div>
</div>
<div className="col-md-4" style={{padding: '15px'}}>
<img style={{width: '100%'}} src="https://tpc.googlesyndication.com/simgad/10559698493288182074"
alt="ads"/>
</div>
</div>
</>
)
};
export default AdsListing;
The issue is you aren't returning a new object reference. You save a reference to the last state object, mutate a property on it, and save it again.
const navigatePage = (pager) => {
const newPagination = pagination; // copy ref pointing to pagination
newPagination.current_page = pager; // mutate property on ref
setPagination(newPagination); // save ref still pointing to pagination
};
In this case the location in memory that is pagination remains static. You should instead copy all the pagination properties into a new object.
const navigatePage = (pager) => {
const newPagination = {...pagination}; // shallow copy into new object
newPagination.current_page = pager;
setPagination(newPagination); // save new object
};
To take it a step further you really should be doing functional updates in order to correctly queue up updates. This is in the case that setPagination is called multiple times during a single render cycle.
const navigatePage = (pager) => {
setPagination(prevPagination => {
const newPagination = {...prevPagination};
newPagination.current_page = pager;
});
};
In the case of pagination queueing updates may not be an issue (last current page set wins the next render battle), but if any state updates actually depend on a previous value then definitely use the functional update pattern,

ReactJS hooks useContext issue

I'm kind of to ReactJS and I'm trying to use useContext with hooks but I'm having some trouble. I've been reading through several articles but I could not understand it.
I understand its purpose, but I can't figure out how to make it work properly. If I'm correct, the purpose is to be able to avoid passing props down to every children and be able to access values from a common provider at any depth of the component tree. This includes functions and state values. Please correct me if I'm wrong.
I've been testing with the following files. This is the ManagerContext.js file:
import { createContext } from 'react';
const fn = (t) => {
console.log(t);
}
const ctx = createContext({
title: 'This is a title',
editing: false,
fn: fn,
})
let ManagerContext = ctx;
export default ManagerContext;
Then I have the LessonManager.js file which is used in my main application:
import React from 'react';
import LessonMenu from './LessonMenu.js';
export default function LessonManager() {
return (
<LessonMenu />
)
}
And finally the LessonMenu.js:
import React from 'react';
import 'rsuite/dist/styles/rsuite.min.css';
import ManagerContext from './ManagerContext.js';
export default function LessonMenu() {
const value = React.useContext(ManagerContext);
return (
<div>
<span>{value.title}</span>
<button
onClick={()=>value.fn('ciao')}
>click</button>
<button
onClick={()=>value.title = 'new title'}
>click</button>
</div>
)
}
In the LessonMenu.js file the onClick={()=>value.fn('ciao')} works but the onClick={()=>value.title = 'new title'} doesn't re render the component.
I know something is wrong, but can someone make it a bit clearer for me?
In order for rerendering to occur, some component somewhere must call setState. Your code doesn't do that, so no rendering happens.
The setup you've done for the ManagerContext creates a default value, but that's only going to get used if you don't render any ManagerContext.Provider in your component tree. That's what you're doing now, but it's almost certainly not what you want to. You'll want to have some component near the top of your tree render a ManagerContext.Provider. This component can will be where the state lives, and among the data it sends down will be a function or functions which set state, thus triggering rerendering:
export default function LessonManager() {
const [title, setTitle] = useState('SomeOtherTitle');
const [editing, setEditing] = useState(false);
const value = useMemo(() => {
return {
title,
setTitle,
editing,
setEditing,
log: (t) => console.log(t)
}
}, [title, editing]);
return (
<ManagerContext.Provider value={value} >
<LessonMenu />
</ManagerContext.Provider/>
)
}
// used like:
export default function LessonMenu() {
const value = React.useContext(ManagerContext);
return (
<div>
<span>{value.title}</span>
<button onClick={() => value.log('ciao')}>
click
</button>
<button onClick={() => value.setTitle('new title')}>
click
</button>
</div>
)
}

Resources