Sharing state between React components - javascript

I have a table component (parent) and in each row of the table, there's another component that's basically an image button (child). When clicked, the image button switches from its default three vertical dots (https://png.icons8.com/windows/1600/menu-2.png) to a download icon. The three vertical dots image has an onClick listener that switches it to the download icon and the download icon has an onClick listener that downloads the file in the table row. Each image button has it's own state (based on which image to display). In the table component, I have a div that wraps the entire screen. When that div is clicked (so basically if you click anywhere outside the images), I want to be able to reset all the images in the table back to the three dots. I'm not sure how to accomplish this since there's many child components, each with it's own state (therefore I don't think redux would work). Is there a way to share the state of each image button with the table component?
Here's some of the table code
<div className='shield' onClick={() => this.resetDownloads()}></div>
<table className='table table-striped'>
<tbody>
this.props.songs.data.map((song, index) =>
<tr key={index}>
<td>{song.name}</td>
<td>{song.artist}</td>
<td>{song.duration}</td>
<td><ActionButton song={song}/></td>
</tr>
</tbody>
</table>
Here's some of the ActionButton component
class ActionButton extends React.Component {
constructor(props) {
super(props);
this.state = {
download: false
};
this.toggle = this.toggle.bind(this);
}
toggle(e) {
e.stopPropagation();
this.setState({
download: !this.state.download
});
}
render() {
return this.state.download ? (
<div>
<img src={Download} width='15%' onClick={(e) => this.props.downloadSong(this.props.song, e)}></img>
</div>
)
: (
<div>
<img src={Dots} width='15%' onClick={(e) => this.toggle(e)} className='image'></img>
</div>
)
}
}

This is where you lift your state up. Actually, in the first place, you don't need a state in your ActionButton component. It should be a stateless component. You can keep all your data in the parent component.
Let's assume there is an id property in the song data. You can track a downloadState in the parent component and add this song's id to this state object. Then you can pass this value to your ActionComponent and use it. Also, you can keep all your functions in your parent component.
const songs = [
{ id: "1", name: "Always Blue", artist: "Chet Baker", duration: "07:33" },
{ id: "2", name: "Feeling Good", artist: "Nina Simone", duration: "02:58" },
{ id: "3", name: "So What", artist: "Miles Davis", duration: "09:23" },
]
class App extends React.Component {
state = {
downloadState: {},
}
toggle = ( e, id ) => {
e.stopPropagation();
this.setState( prevState => ({
downloadState: { ...prevState.downloadState, [id]: !prevState.downloadState[id]}
}))
}
downloadSong = ( e, song ) => {
e.stopPropagation();
alert( song.name );
}
resetDownloads = () => this.setState({ downloadState: {}});
render() {
return (
<div onClick={this.resetDownloads}>
<table>
<tbody>
{
songs.map((song, index) => (
<tr key={index}>
<td>{song.name}</td>
<td>{song.artist}</td>
<td>{song.duration}</td>
<td>
<ActionButton
toggle={this.toggle}
song={song}
downloadState={this.state.downloadState}
downloadSong={this.downloadSong}
/>
</td>
</tr>
))
}
</tbody>
</table>
</div>
)
}
}
const ActionButton = props => {
const { downloadState, downloadSong, song, toggle } = props;
const handleToggle = e => toggle(e, song.id);
const handleDownload = e => downloadSong( e, song );
const renderImages = () => {
let images = "";
if ( downloadState[song.id] ) {
images = <p onClick={handleDownload}>Download</p>;
} else {
images = <p onClick={handleToggle}>Dots</p>;
}
return images;
}
return (
<div>{renderImages()}</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
table, th, td {
border: 1px solid black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
If there isn't any id property then you can set up the same logic with indexes but I think every data should have an id :) Maybe instead of using indexes same logic can be used with song names since they are almost unique. Who knows :)

Related

how to delete a row from the table with a button on react?

I wanted to remove a row from the table with the function 'deleteRow(btn)' when pressing the button, but I get this error 'Cannot read properties of undefined (reading 'parentNode')'. What could I add or correct to successively drop a row from a table?
App.js
class App extends React.Component{
constructor(props) {
super(props);
this.state = {
fotos: [],
restaurantes:[],
}
}
deleteRow=(btn)=> {
var row = btn.parentNode.parentNode;
row.parentNode.removeChild(row);
}
render(){
const { fotos, restaurantes } = this.state;
<div className="container">
<Tabela dadosFotos={fotos} restaurante={this.deleteRow} />
</div>
}
Tabela.js
import React from "react";
const CorpoTabela = (props) => {
const rows = props.dadosDasFotos.map((row) => {
return(
<tr key={row.idFoto}>
<td>{row.nomeRestaurante}</td>
<td>
<button className="btn btn-outline-danger"
onClick={()=>props.restauranteAremover(row.idFoto)}>
Delete restaurante
</button>
</td>
</tr>
)
})
return(<tbody>{rows}</tbody>)
}
class Tabela extends React.Component{
render(){
const { dadosFotos, restaurante }=this.props
return(
<table className="table table-striped">
<CorpoTabela dadosDasFotos={dadosFotos} restauranteAremover={restaurante}/>
</table>
)
}
}
You should not be manipulating the DOM as this is an anti-pattern in React, instead you should update the state you are rendering from.
Delete by idFoto.
deleteRow = (idFoto)=> {
this.setState(prevState => ({
fotos: prevState.fotos.filter(el => el.idFoto !== idFoto
}))
}
In the child pass the id to the delete handler.
<button className="btn btn-outline-danger"
onClick={() => props.restauranteAremover(row.idFoto)}>
Delete restaurante
</button>
In React, you generally want to try avoiding direct DOM manipulation, since this takes state management out of React, which is something you want to avoid.
Therefore, instead of trying to delete each row directly using DOM functions like remove or removeChild, it would be best to keep all of the table rows in a key in the state object. Then, you can filter out the deleted row by filtering it out by index or through some other identifier. Here's an example:
import { Component } from 'react'
import './styles.css'
export default class App extends Component {
state = {
rows: [
{ id: 1, col1: 'A', col2: 'some text' },
{ id: 2, col1: 'B', col2: 'some text' }
]
}
spliceRow = (index) => {
this.state.rows.splice(index, 1)
this.setState({ rows: this.state.rows })
}
filterRows = (id) => {
this.setState({
rows: this.state.rows.filter((row) => {
return row.id !== id
})
})
}
render() {
return (
<table className="App">
<tbody>
{this.state.rows.map((row, index) => {
return (
<tr key={row.id}>
<td>{row.col1}</td>
<td>{row.col2}</td>
<td>
<button onClick={() => this.spliceRow(index)}>
Remove row with splice
</button>
</td>
<td>
<button onClick={() => this.filterRows(row.id)}>
Remove row with filter
</button>
</td>
</tr>
)
})}
</tbody>
</table>
)
}
}

ReactJS: How to get the data of a table row that has changed

I have a main Table component that maintains the table's state. I have a dumb component which gets props from the main component. I use it to render the table row layout. I am trying to make this table editable. For this reason, I need a way to find out which tr was edited. Is there a way to get access to the tr key using which I can get access to the whole object?
No you can't get the value of a key in a child prop. From the docs:
Keys serve as a hint to React but they don’t get passed to your
components. If you need the same value in your component, pass it
explicitly as a prop with a different name
const content = posts.map((post) =>
<Post
key={post.id}
id={post.id}
title={post.title} />
);
A possible solution right of my head might be the following:
import React from 'react';
class Table extends React.Component {
constructor(props) {
super(props);
this.state = {
rows: [
{
id: 0,
title: "ABC"
},
{
id: 1,
title: "DEF"
},
{
id: 2,
title: "GHI"
}
]
}
}
render() {
return <table>
<tbody>
{
this.state.rows.map((item) => <Row key={item.id} item={item} updateItem={this.updateItem} />)
}
</tbody>
</table>
}
updateItem = (newItemData) => {
const index = this.state.rows.findIndex((r) => r.id == newItemData.id);
let updatedRows = this.state.rows;
updatedRows.splice(index, 1, newItemData);
this.setState({
rows: updatedRows
});
}
}
const Row = ({item, updateItem}) => {
const [title, setValue] = React.useState(item.title);
return <tr>
<td>{item.id}</td>
<td>
<input type="text" value={title} onChange={(e) => setValue(e.currentTarget.value)} />
</td>
<td>
<button onClick={() => updateItem({...item, title})}>Save</button>
</td>
</tr>
};
If you want to send from nested component to parent some data use a callback function

Easy communication of image between siblings

I'm new to ReactJS and I would like to communicate between my components.
When I click an image in my "ChildA" I want to update the correct item image in my "ChildB" (type attribute in ChildA can only be "itemone", "itemtwo", "itemthree"
Here is what it looks like
Parent.js
export default class Parent extends Component {
render() {
return (
<div className="mainapp" id="app">
<ChildA/>
<ChildB/>
</div>
);
}
}
if (document.getElementById('page')) {
ReactDOM.render(<Builder />, document.getElementById('page'));
}
ChildA.js
render() {
return _.map(this.state.eq, ecu => {
return (
<img src="../images/misc/ec.png" type={ecu.type_eq} onClick={() => this.changeImage(ecu.img)}/>
);
});
}
ChildB.js
export default class CharacterForm extends Component {
constructor(props) {
super(props);
this.state = {
items: [
{ name: "itemone" image: "defaultone.png"},
{ name: "itemtwo" image: "defaulttwo.png"},
{ name: "itemthree" image: "defaultthree.png"},
]
};
}
render() {
return (
<div className="items-column">
{this.state.items.map(item => (<FrameCharacter key={item.name} item={item} />))}
</div>
);
}
}
I can retrieve the image on my onClick handler in my ChildA but I don't know how to give it to my ChildB. Any hints are welcomed, thanks you!
What you need is for Parent to pass an event handler down to ChildA which ChildA will call when one of the images is clicked. The event handler will call setState in Parent to update its state with the given value, and then Parent will pass the value down to ChildB in its render method.
You can see this working in the below example. Since I don't have any actual images to work with—and to keep it simple—I've used <button>s instead, but the principle is the same.
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
clickedItem: 'none',
};
}
render() {
return (
<div>
<ChildA onClick={this.handleChildClick}/>
<ChildB clickedItem={this.state.clickedItem}/>
</div>
);
}
handleChildClick = clickedItem => {
this.setState({ clickedItem });
}
}
const items = ['item1', 'item2', 'item3'];
const ChildA = ({ onClick }) => (
<div>
{items.map(name => (
<button key={name} type="button" onClick={() => onClick(name)}>
{name}
</button>
))}
</div>
);
const ChildB = ({clickedItem}) => (
<p>Clicked item: {clickedItem}</p>
);
ReactDOM.render(<Parent/>, document.querySelector('div'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.development.js"></script>
<div></div>

React click on item to show details

Iam new to React and I'm trying to interact with the swapi API.
I want to get the list of films (movie titles list) and when I click on a title to show the opening_crawl from the json object.
I managed to get the film titles in an array. I don't know how to proceed from here.
Here is my code:
class StarWarsApp extends React.Component {
render() {
const title = "Star Wars";
const subtitle = "Movies";
return (
<div>
<Header title={title} />
<Movies />
</div>
);
}
}
class Header extends React.Component {
render() {
return (
<div>
<h1>{this.props.title}</h1>
</div>
);
}
}
class Movies extends React.Component {
constructor(props) {
super(props);
this.handleMovies = this.handleMovies.bind(this);
this.state = {
movies: []
};
this.handleMovies();
}
handleMovies() {
fetch("https://swapi.co/api/films")
.then(results => {
return results.json();
})
.then(data => {
console.log(data);
let movies = data.results.map(movie => {
return <div key={movie.episode_id}>{movie.title}</div>;
});
this.setState(() => {
return {
movies: movies
};
});
});
}
render() {
return (
<div>
<h1>Episodes</h1>
<div>{this.state.movies}</div>
</div>
);
}
}
ReactDOM.render(<StarWarsApp />, document.getElementById("app"));
To iterate over movies add this in render method:
render(){
return (
<div>
<h1>Episodes</h1>
{
this.state.movies.map((movie, i) => {
return (
<div className="movie" onClick={this.handleClick} key={i}>{movie.title}
<div className="opening">{movie.opening_crawl}</div>
</div>
);
})
}
</div>
);
}
Add this method to your Movies component to add active class on click to DIV with "movie" className:
handleClick = event => {
event.currentTarget.classList.toggle('active');
}
Include this css to your project:
.movie .opening {
display: none;
}
.active .opening {
display: block
}
After fetching the data, just keep it in your state then use the pieces in your components or JSX. Don't return some JSX from your handleMovies method, just do the setState part there. Also, I suggest using a life-cycle method (or hooks API maybe if you use a functional component) to trigger the fetching. By the way, don't use class components unless you need a state or life-cycle methods.
After that, you can render your titles in your render method by mapping the movies state. Also, you can have a place for your opening_crawls part and render it with a conditional operator. This condition changes with a click. To do that you have an extra state property and keep the movie ids there. With the click, you can set the id value to true and show the crawls.
Here is a simple working example.
const StarWarsApp = () => {
const title = "Star Wars";
const subtitle = "Movies";
return (
<div>
<Header title={title} />
<Movies />
</div>
);
}
const Header = ({ title }) => (
<div>
<h1>{title}</h1>
</div>
);
class Movies extends React.Component {
state = {
movies: [],
showCrawl: {}
};
componentDidMount() {
this.handleMovies();
}
handleMovies = () =>
fetch("https://swapi.co/api/films")
.then(results => results.json())
.then(data => this.setState({ movies: data.results }));
handleCrawl = e => {
const { id } = e.target;
this.setState(current => ({
showCrawl: { ...current.showCrawl, [id]: !current.showCrawl[id] }
}));
};
render() {
return (
<div>
<h1>Episodes</h1>
<div>
{this.state.movies.map(movie => (
<div
key={movie.episode_id}
id={movie.episode_id}
onClick={this.handleCrawl}
>
{movie.title}
{this.state.showCrawl[movie.episode_id] && (
<div style={{ border: "1px black solid" }}>
{movie.opening_crawl}
</div>
)}
</div>
))}
</div>
</div>
);
}
}
ReactDOM.render(<StarWarsApp />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I am using id on the target div to get it back from the event object. I don't like this method too much but for the sake of clarity, I used this. You can refactor it and create another component may be, then you can pass the epoisde_id there and handle the setState part. Or you can use a data attribute instead of id.

Use Buttons to trigger filter function on react-table in React

I don't know how to word this. I am learning React and I have data loaded into React-Table via fetch. I tried using React-Table and just custom plain divs and tables.
I want to create a touch buttons of the alphabet from A, B, C, D ... Z. Those buttons should call the filter for the letter that is in the button. So, for example the buttons are the following.
// In Directory.js
class FilterButtons extends React.Component {
alphaFilter(e) {
console.log(e.target.id);
// somehow filter the react table
}
render() {
return (
<div>
<button onClick={this.alphaFilter} id="A" className="letter">A</button>
<button onClick={this.alphaFilter} id="B" className="letter">B</button>
<button onClick={this.alphaFilter} id="C" className="letter">C</button>
</div>
);
}
}
const BottomMenu = props => (
<div className="btm-menu">
<div className="toprow">
<div className="filter-keys">
<FilterButtons />
</div>
</div>
</div>
);
// I have a class Directory extends Component that has the BottomMenu in it
// I also have a DataGrid.js with the React Table in there
class DataGrid extends React.Component {
constructor() {
super();
this.state = {
data: [],
};
}
componentWillMount() {
fetch('http://localhost:3000/rooms.json').then((results) => results.json()).then((data) => {
console.log(data.room);
this.setState({
data: data.room
})
})
}
render() {
const { data } = this.state;
return (
<div>
<ReactTable
data={data}
filterable
defaultFilterMethod={(filter, row) =>
String(row[filter.id]) === filter.value}
columns={[
{
Header: "Name",
accessor: "dName",
filterMethod: (filter, row) =>
row[filter.id].startsWith(filter.value)
},
{
Header: "Department",
accessor: "dDept"
},
{
Header: "Room",
accessor: "dRoom"
},
{
Header: "Map",
accessor: "dRoom",
id: "over",
}
]
}
defaultPageSize={14}
className="-striped -highlight"
/>
<br />
</div>
);
}
}
export default DataGrid;
At this point I am unsure what to do to get the button click of one of the A, B, C letters to filter the React Table. I do not want the Input field option that is always used because I want only buttons as the user will not have a keyboard, only touch.
Basically, React Table or just any table that can be filtered by clicking buttons with a letter that gets passed back to the filter. If I was using JQuery I would use a button click and then filter that way. I still haven't learned all the ins and outs of React and how to get this done. I also want to use external buttons to sort but that should be easier, hopefully.
Thanks.
Sorry if all of this doesn't make sense, I am just trying to lay it out. Again, no keyboard, only touch on a touch screen so the input field isn't going to work for me.
For React-Table filter to be controlled externally by buttons, you should take a look at the Controlled Table example. Then the table component becomes a controlled component.
There, you can set the state of the table filter externally and use both of the props:
<ReactTable ...(your other props)
filtered={this.state.filtered}
onFilteredChange={filtered => this.setState({ filtered })}
/>
The filtered prop will monitor change in the state. So whenever you update its value through your letter buttons via e.g.
this.setState({filtered: { id: "dName", value: "A"}})
the table's filter will get updated. Also, onFilteredChange should work the other direction, namely the embedded filtering of the react-table can update the local state. So that you can monitor it and use its value within your DataGrid component.
One other option could be to avoid using local states and implement it in redux, though. Because states hanging around components are eventually becoming source of errors and increasing complexity of debugging.
UPDATE -- As per the question owner's comment for more details:
In order to use FilterButtons and DataGrid together, you can define a container component that encapsulates both FilterButtons and DataGrid.
The container component keeps the shared state, and the filtering function operates on the state function. But data will still reside within the datagrid.
class DataGridWithFilter extends React.Component {
// you need constructor() for binding alphaFilter to this.
// otherwise it cannot call this.setState
constructor(props) {
super(props);
this.alphaFilter = this.alphaFilter.bind(this);
}
// personally i do not use ids for passing data.
// therefore introduced the l parameter for the letter
alphaFilter(e,l) {
console.log(e.target.id);
this.setState({filtered: { id: "dName", value: l}});
}
render(){
return <div>
<DataGrid filtered={this.state.filtered}
filterFunc={this.alphaFilter}
</DataGrid>
<BottomMenu filtered={this.state.filtered}
filterFunc={this.alphaFilter} />
</div>
}
}
Also this above thing requires the use of prop filterFunc from within BottomMenu and FilterButtons components.
class FilterButtons extends React.Component {
render() {
const {props} = this;
return (
<div>
<button onClick={(e) => props.filterFunc(e, "A")} id="A" className="letter">A</button>
<button onClick={(e) => props.filterFunc(e, "B")} id="B" className="letter">B</button>
<button onClick={(e) => props.filterFunc(e, "C")} id="C" className="letter">C</button>
</div>
);
}
}
const BottomMenu = props => (
<div className="btm-menu">
<div className="toprow">
<div className="filter-keys">
<FilterButtons filterFunc = {props.filterFunc} />
</div>
</div>
</div>
);
class DataGrid extends React.Component {
constructor() {
super();
this.state = {
data: [],
};
}
componentWillMount() {
fetch('http://localhost:3000/rooms.json').then((results) => results.json()).then((data) => {
console.log(data.room);
this.setState({
data: data.room
})
})
}
render() {
const { data } = this.state;
return (
<div>
<ReactTable
data={data}
filterable
defaultFilterMethod={(filter, row) =>
String(row[filter.id]) === filter.value}
columns={[
{
Header: "Name",
accessor: "dName",
filterMethod: (filter, row) =>
row[filter.id].startsWith(filter.value)
},
{
Header: "Department",
accessor: "dDept"
},
{
Header: "Room",
accessor: "dRoom"
},
{
Header: "Map",
accessor: "dRoom",
id: "over",
}
]
}
defaultPageSize={14}
className="-striped -highlight"
filtered = {this.props.filtered}
onFilteredChange = {filtered => this.props.filterFunc({filtered})}
/>
<br />
</div>
);
}
}
I have not checked against typo etc but this should work.
In React, you can update components when state changes. The only way to trigger is to use this.setState()
So I would change my state object something like this:
state = {
date: [],
filter: ""
};
so here is the new file:
// In Directory.js
class FilterButtons extends React.Component {
alphaFilter = (word) => {
this.setState({
filter: word
});
};
render() {
return (
<div>
<button onClick={() => this.alphaFilter("A")} id="A" className="letter">A</button>
<button onClick={() => this.alphaFilter("B")} id="B" className="letter">B</button>
<button onClick={() => this.alphaFilter("C")} id="C" className="letter">C</button>
</div>
);
}
}
const BottomMenu = props => (
<div className="btm-menu">
<div className="toprow">
<div className="filter-keys">
<FilterButtons />
</div>
</div>
</div>
);
right after you call alphaFilter() your component will update. And when you put your filter function in it it's going display as expected. So I would choose the .map() function of array. You can either use filter() or return after comparing data in map()
For react-table 7 you can listen for input changes outside and call setFilter:
useEffect(() => {
// This will now use our custom filter for age
setFilter("age", ageOutside);
}, [ageOutside]);
See https://codesandbox.io/s/react-table-filter-outside-table-bor4f?file=/src/App.js

Categories