React/Redux: separating presentational and container components dilemma. - javascript

Here's a simplified representation of what I have:
container.js:
import Presentation from './Presentation';
import { connect } from 'react-redux';
export default connect(
({ someState}) => ({ ...someState }),
(dispatch) => {
onClick(e) => ({})
}
)(Presentation);
pesentation.js:
export default ({ items, onClick}) => (
<table>
<tbody>
{items.map((item, index) => (
<tr>
<td onClick={onClick}>{item.a}</td>
<td>
<div onClick={onClick}>{item.b}</div>
</td>
</tr>
))}
</tbody>
</table>
);
Let's say, I want to have access to index variable of each row in handler.
First solution that comes into mind is to use a closure for capturing index:
container.js:
onClick(e, index) => ({});
presentation.js:
<td onClick={e => onClick(e, index)}>{item.a}</td>
<td>
<div onClick={e => onClick(e, index)}>{item.b}</div>
</td>
It works, but it looks quite dirty, so I decided to find another solution if possible.
The next thing is to use data-index attribute on each element:
container.js:
onClick({ target: { dataset: { index } } }) => ({});
presentation.js:
<td data-index={index} onClick={onClick}>{item.a}</td>
<td>
<div data-index={index} onClick={onClick}>{item.b}</div>
</td>
Maybe it's better, but the need to use data-attribute with same value on multiple elements looks redundant. What if I have 20 elements?
Ok, no problem! Let's just move it to parent container:
container.js:
onClick(e) => {
const {index} = e.target.closest('tr').dataset;
};
presentation.js:
<tr data-index={index}>
<td onClick={onClick}><td>
<td>
<div onClick={onClick}>{item.b}</div>
</td>
</tr>
Ok, the next problem is solved. But wait. This way, container heavily relies on presentational component layout. It becomes coupled to view by introducing reference to tr tag. What if at some point tag will be changed to, for example, div?
The final solution that I come up with is to add a CSS class for container tag element, which is passed from container component to presentation:
container.js:
import Presentation from './Presentation';
import { connect } from 'react-redux';
const ITEM_CONTAINER_CLASS = 'item-cotainer';
export default connect(
({ someState }) => ({ ...someState, ITEM_CONTAINER_CLASS }),
(dispatch) => {
onClick(e) => {
const {index} = e.target.closest(`.${ITEM_CONTAINER_CLASS }`).dataset;
}
}
)(Presentation);
pesentation.js:
export default ({ items, onClick, ITEM_CONTAINER_CLASS }) => (
<table>
<tbody>
{items.map((item, index) => (
<tr className={ITEM_CONTAINER_CLASS}>
<td onClick={onClick}>{item.a}</td>
<td>
<div onClick={onClick}>{item.b}</div>
</td>
</tr>
))}
</tbody>
</table>
);
But still, it has one small disadvantage: it requires a DOM manipulation to get index.
Thanks for reading (if someone). I wonder, if there are another options?

This is classical React/Redux dilemma. My answer would be to extract your "item view" to a component and add an onItemClick prop to it. Of course your item view should receive the item to display, and will be able to send it as a parameter to your container.
The e parameter belongs to your presentation, not to your action creator which is business logic. And storing data in the DOM (like using data- attributes is not needed with React at all).
Something like this (I tend to start from bottom to top, from simple to complex):
//Item.js
export default class Item extends Component {
constructor(props){
super(props)
this.handleClick = this.handleClick.bind(this)
}
handleClick(e){
e.preventDefault() //this e stays here
this.props.onItemClick(this.props.item)
}
render(){
const { item } = this.props
return (
<tr className={ITEM_CONTAINER_CLASS}>
<td onClick={this.handleClick}>{item.a}</td>
<td onClick={this.handleClick}>{item.b}</td>
</tr>
)
}
}
// ItemList (Presentation)
export default ItemList = ({ items, onClick}) => (
<table>
<tbody>
{
items.map((item, index) =>
<Item item={item} onItemClick={ onClick } />
)
}
</tbody>
</table>
)
// Container.js - just change your mapDispatchToProps to accept the full item, or whatever you're interested in (perhaps only the id)
import ItemList from './ItemList';
import { connect } from 'react-redux';
export default connect(
({ someState}) => ({ ...someState }),
{
onClick(item){
return yourActionCreatorWith(item.id)
}
}
)(Presentation);
That way you have everything you need right when you need to dispatch. In addition, creating your "item view" as a separate component will allow you to test it in isolation and even reuse it if needed.
It may seem much more work but trust me, it pays off.

See also an accepted answer.
Here's the solution:
container.js
import Presentation from './presentation';
import { connect } from 'react-redux';
export default connect(
({ someState }) => ({ ...someState }),
(dispatch) => {
onClick(index) => {}
}
)(Presentation);
presentation.js
import Item from './item.js';
export default ({ items, onClick }) => (
<table>
<tbody>
{items.map((item, index) => <Item index={index} item={item} onClick={onClick} />)}
</tbody>
</table>
);
item.js
export default ({ index, item, onClick }) => {
const onItemClick = () => onClick(index);
return (
<tr>
<td onClick={onItemClick}>{item.a}</td>
<td>
<div onClick={onItemClick}>{item.b}</div>
</td>
</tr>
)
};

Related

How to call a function in one React component by button click in another component

I am making a pos system and I am trying to make the items appear on the list section when I click on each of them. My problem is that the Items section and the List section are two different components and I can not figure out how to make a click in Items section change the state of the list section.
Items Section:
import Item from "./Item";
function ItemSection() {
const Items = [{ title: "DOMAIN", cost: 109 }];
return (
<div>
<Item title={Items[0].title} cost={Items[0].cost} onClick={() => this.clickHandler(title, cost)}></Item>
</div>
);
}
export default ItemSection;
List Section:
import React, { useState } from "react";
function ListItems(props) {
const [title, setTitle] = useState(props.title);
const [cost, setCost] = useState(props.cost);
const clickHandler = (mTitle, mCost) => {
setTitle(mTitle);
setCost(mCost);
};
return (
<div>
<table>
<tr>
<td> {title} </td>
<td> ${cost} </td>
</tr>
</table>
</div>
);
}
export default ListItems;
Item:
function Item(props) {
return (
<div>
<button>{props.title}</button>
</div>
);
}
export default Item;
As per my understanding, you've two components: ItemSection and ListItems and you want to call a function in ListItems, that is defined in ItemSection? Is it the correct understanding?
If yes, then you can define your function in ItemSection and send that function as prop in ItemSection
In ItemSection:
<ListItems sampleFunction = {(e)=>sampleFunction(e)}/>
In ListItems:
interface Props{
sampleFunction: Function;
}
and:
<button OnClick = {props.sampleFunction}>{props.title}</button>
This is just a pseudo code.

How to control JSX component using filter() in React?

I'm currently using child components which returns JSX.
//PARENT COMPONENT
import ApprovalTableElement from './E_Approval_Element.js';
//JSX of E_Approval_Element.js
const [approvalElement, setApprovalElement] = useState([ApprovalTableElement]);
//Add one more approval column
const addApprovalSpace = () => {
setApprovalElement([...approvalElement, ApprovalTableElement]);
};
return (
<div className={styles['EApprovalWriteDiv']}>
<div className={styles['authDiv']}>
{approvalElement.map((element, index) => (
<button>DELETE ONE APPROVAL SECTION</button>
<ApprovalTableElement index={index} />
))}
</div>
</div>
);
};
export default E_Approval_Write;
//CHILD COMPONENT
function ApprovalTableElement() {
return (
<>
<table className={styles['approvalTable']}>
<tbody className={styles['approval']}>
<tr className={styles['name']}>
<th>
<select style={{ marginLeft: 10 }}>
<option>선택</option>
<option>결재자</option>
<option>합의자</option>
</select>
</th>
</tr>
<tr className={styles['signature']}>
<td>
<div>SIGN</div>
</td>
</tr>
<tr className={styles['name']} onClick={memberModalTrigger}>
<td>
<Typography variant='button' display='block'>
</Typography>
</td>
</tr>
</tbody>
</table>
</>
);
}
export default ApprovalTableElement;
with this code, what I'm trying to do is using
{approvalElement.map((element, index) => (
<button>DELETE ONE APPROVAL SECTION</button>
<ApprovalTableElement index={index} />
))}
this button, deleting selected ApprovalTableElement.
right now, I have this UI. When I click + button, I keeps adding component. But when I click remove button, the table attached to the button should disappear. BUT not the other ones.
All I can know is the index of the Component, so I am really confusing on how to delete the targeted component using filter().
OR, should I add button tag inside the CHILD COMPONENT not the PARENT COMPONENT?
However, If I can make what I'm trying to do with this code, please tell me how to set up the code properly to make things possible. Thank you!
Just pick those which id is different from the one you are deleting
const removeApprovalSpace = (id) => {
setApprovalElement(items => items.filter(item => item.id !== id));
};
//usage
<button onClick={() => removeApprovalSpace(id)}>Remove</button>
If you don't have id's you can use index
const removeApprovalSpace = (index) => {
setApprovalElement(items => items.filter((item, i) => i !== index));
};
//usage
<button onClick={() => removeApprovalSpace(index)}>Remove</button>

Delete row from a table, deleteRow(id), remove() or use useState () in Reactjs

I have a component with a table that is loaded with the data sent by the REST API, my question more than anything, is if it is good to delete a row with remove () or deleteRow (index) entering the DOM or I have to manipulate the DOM through a state (useState) for good practice or something like that.
Additional info: I have gotten used to manipulating the DOM directly with pure javascript when making static web pages, but in React I think you have to use props, state, hooks to manipulate the DOM elements or am I wrong? there must be cases I guess.
DOM TRAVERSING
export default function App() {
const data = ['Eli','Smith', 'Jhon']
const handleDelete = (index,e) => {
//I can save an ID in <tr> through datasets (data-)
//to get it by traversing the DOM when the user clicks and delete it
//also in my database (API), that's Ok?
e.target.parentNode.parentNode.parentNode.deleteRow(index)
}
const rows = data.map((item, index) => {
return (
<tr key={index}>
<td>{item}</td>
<td><button onClick={e => handleDelete(index,e)}>Delete</button></td>
</tr>
)
})
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<table>
{rows}
</table>
</div>
);
}
That's OK ?
Or do I have to put the rows in a state (useState) and update it by removing the and re-rendering?
Best practice is to manage data through state, I would suggest not to do DOM manipulation manually
export default function App() {
const [data, setData] = useState(['Eli','Smith', 'Jhon']);
const handleDelete = (index,e) => {
setData(data.filter((v, i) => i !== index));
}
const rows = data.map((item, index) => {
return (
<tr key={index}>
<td>{item}</td>
<td><button onClick={e => handleDelete(index,e)}>Delete</button></td>
</tr>
)
})
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<table>
{rows}
</table>
</div>
);
}
use hook useState to save your data and after that with handlerDelete remove the element and set the data. React will re-render component (because your state has been changed) and you get necessary result:
https://codesandbox.io/s/cranky-surf-iykn7?file=/src/App.js

React Matrix of Radio buttons

I tried to solve this js react problem and get stuck on questions 2-4.
Question 2: I don't know how to check the local state for each row in order to check for the duplicate rank select
Question 3: Should I need props passed to the component to check for unique?
Question 4: How do I check all rows have a select ranked and unique?
Here are the questions:
Adding a class of "done" to a row will highlight it green. Provide this
visual feedback on rows which have a selected rank.
Adding a class of "error" to a row will highlight it red. Provide this
visual feedback on rows which have duplicate ranks selected.
There is a place to display an error message near the submit button. Show
this error message: Ranks must be unique whenever the user has selected the
same rank on multiple rows.
The submit button is disabled by default. Enable it when all rows have a
rank selected and all selected ranks are unique.
The orginal App.js
import React, { Component } from 'react';
import './App.css';
import MainPage from './components/MainPage';
class App extends Component {
render() {
return (
<MainPage />
);
}
}
export default App;
MainPage.js
import React from 'react';
import _ from 'lodash';
import FormRow from './FormRow.jsx';
import Animal from './Animal.js';
class MainPage extends React.Component {
constructor(props) {
super(props);
this.state = {
animals: ['panda','cat','capybara','iguana','muskrat'].map((name) => {
return new Animal(name);
}),
error: ''
};
}
render() {
const rows = this.state.animals.map((animal) => {
return (
<FormRow
animalName={animal.name}
key={animal.name}
/>
);
});
const headers = _.range(1, 6).map((i) => <th key={`header-${i}`}>{i}</th>);
return (
<div>
<table>
<thead>
<tr>
<th></th>
{headers}
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
<div>{this.state.error}</div>
<input type="submit" />
</div>
);
}
}
export default MainPage;
FormRow.jsx
import React from 'react';
import _ from 'lodash';
class FormRow extends React.Component {
render() {
const cells = _.range(1, 6).map((i) => {
return (
<td key={`${this.props.animalName}-${i}`}>
<input
type="radio"
name={this.props.animalName}
value={i}
/>
</td>
);
});
return (
<tr>
<th>{this.props.animalName}</th>
{cells}
</tr>
)
}
}
export default FormRow;
Animal.js
class Animal {
constructor(name, rank) {
this.name = name;
this.rank = rank;
}
}
export default Animal;
My code is at GitHub (git#github.com:HuydDo/js_react_problem-.git). Thanks for your suggestion!
FormRow.jsx
import React from 'react';
import _ from 'lodash';
class FormRow extends React.Component {
constructor(){
super();
this.state = {
rowColor : false,
name: "",
rank: 0
// panda: 0,
// cat: 0,
// capybara: 0,
// iguana: 0,
// muskrat: 0
}
}
handleChange = (e) => {
if (this.state.rank === e.target.value){
console.log("can't select same rank.")
}
console.log(e.target.name)
console.log(e.target.value)
this.setState({
// [e.target.name]: e.target.value,
name: e.target.name,
rank: e.target.value,
rowColor: true
}, console.log(this.state))
}
handleChange2 = (e) => {
let newName = e.target.name
let newRank = e.target.value
let cRank = this.state.rank
let cName = this.state.name
console.log(this.state)
console.log(`${newRank} ${newName}`)
if(cName !== newName) {
if(cRank !== newRank) {
this.setState({
name : newName,
rank: newRank,
rowColor: true
},()=> console.log(this.state))
}
else {
console.log("can't select same rank")
}
}
// this.setState(previousState => {
// let cRank = previousState.rank
// let cName = previousState.name
// console.log(previousState)
// return {
// rank: newRank,
// name: newName,
// rowColor: true
// }
// },console.log(this.state.rank))
}
render() {
const cells = _.range(1, 6).map((i) => {
return (
<td key={`${this.props.animalName}-${i}`} onChange={this.handleChange2}>
<input
type="radio"
name={this.props.animalName}
value={i}
/>
</td>
);
});
return (
<tr className = {(this.state.rowColor) ? 'done':null} >
{/* <tr> */}
<th>{this.props.animalName}</th>
{cells}
</tr>
)
}
}
export default FormRow;
MainPage.jsx
import React from 'react';
import _ from 'lodash';
import FormRow from './FormRow.jsx';
import Animal from './Animal.js';
class MainPage extends React.Component {
constructor(props) {
super(props);
this.state = {
animals: ['panda','cat','capybara','iguana','muskrat'].map((name) => {
return new Animal(name);
}),
error: ''
};
}
getValue = ({name,rank}) =>{
console.log(`Name: ${name} rank: ${rank}`)
}
// handleSubmit = event => {
// event.preventDefault()
// this.props.getValue(this.state)
// }
checkForUnique = () => {
// Show this error message: `Ranks must be unique` whenever the user has selected the
// same rank on multiple rows.
this.setState({
error : "Ranks must be unique"
})
}
isDisabled = () =>{
// The submit button is disabled by default. Enable it when all rows have a
// rank selected and all selected ranks are unique.
return true
}
render() {
const rows = this.state.animals.map((animal) => {
return (
<FormRow
animalName={animal.name}
key={animal.name}
rank={animal.rank}
handleChange={this.handleChange}
getValue={this.getValue}
/>
);
});
const headers = _.range(1, 6).map((i) => <th key={`header-${i}`}>{i}</th>);
return (
<div>
{/* <form onSubmit={this.onSubmit}> */}
<table>
<thead>
<tr>
<th></th>
{headers}
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
<div>{this.state.error}</div>
<input type="submit" value="Submit" disabled={this.isDisabled()} /> {/* <button type="submit">Submit</button> */}
{/* </form> */}
</div>
);
}
}
export default MainPage;
enter image description here
I tried to add handleChange and handleAnimalSelect methods, but I get an error. The new name and rank are not added to the arrays.
MainPage.jsx
import React from 'react';
import _ from 'lodash';
import FormRow from './FormRow.jsx';
import Animal from './Animal.js';
class MainPage extends React.Component {
constructor(props) {
super(props);
this.state = {
animals: ['panda','cat','capybara','iguana','muskrat'].map((name) => {
return new Animal(name);
}),
error: ''
};
}
isDisabled = () =>{
// The submit button is disabled by default. Enable it when all rows have a
// rank selected and all selected ranks are unique.
return true
}
render() {
const rows = this.state.animals.map((animal) => {
return (
<FormRow
animalName={animal.name}
key={animal.name}
rank={animal.rank}
getValue={this.getValue}
handleAnimalSelect={this.handleAnimalSelect}
/>
);
});
const headers = _.range(1, 6).map((i) => <th key={`header-${i}`}>{i}</th>);
return (
<div>
{/* <form onSubmit={this.onSubmit}> */}
<table>
<thead>
<tr>
<th></th>
{headers}
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
<div>{this.state.error}</div>
<input type="submit" value="Submit" disabled={this.isDisabled()} />
{/* <button type="submit">Submit</button> */}
{/* </form> */}
</div>
);
}
}
export default MainPage;
FormRow.jsx
import React from 'react';
import _ from 'lodash';
import FormRow from './FormRow.jsx';
import Animal from './Animal.js';
class MainPage extends React.Component {
constructor(props) {
super(props);
this.state = {
animals: ['panda','cat','capybara','iguana','muskrat'].map((name) => {
return new Animal(name);
}),
error: ''
};
}
isDisabled = () =>{
// The submit button is disabled by default. Enable it when all rows have a
// rank selected and all selected ranks are unique.
return true
}
render() {
const rows = this.state.animals.map((animal) => {
return (
<FormRow
animalName={animal.name}
key={animal.name}
rank={animal.rank}
getValue={this.getValue}
handleAnimalSelect={this.handleAnimalSelect}
/>
);
});
const headers = _.range(1, 6).map((i) => <th key={`header-${i}`}>{i}</th>);
return (
<div>
{/* <form onSubmit={this.onSubmit}> */}
<table>
<thead>
<tr>
<th></th>
{headers}
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
<div>{this.state.error}</div>
<input type="submit" value="Submit" disabled={this.isDisabled()} />
{/* <button type="submit">Submit</button> */}
{/* </form> */}
</div>
);
}
}
export default MainPage;
You're pretty much making a form with a few rows of multiple choice answers.
One way of simplifying everything is to have all the logic in the top component, in your case I think MainPage would be where it would be. Pass down a function as a prop to all the descendants that allows them to update the form data upstream.
In Q2, how do intend to check the state for each row? Perhaps you can use arrays or objects to keep track of the status of each question. The arrays/objects are stored in state, and you just check them to see what the status is.
I'm actually not clear what your app looks like - what does a row look like? (You might want to post a screenshot) And I don't see any way for rank to be selected - I don't even see what the ranks are for, or how they are used in the form. So perhaps your form design needs to be tweaked. You should begin the form design with a clear picture in YOUR mind about how the app will work. Maybe start by drawing the screens on paper and drawing little boxes that will represent the objects/array variables and go through the process of a user using your app. What happens to the various boxes when they click radio buttons and so on. How will you know if the same rank is selected twice - where are the selected ranks stored? What animals are clicked/selected? Where are those stored? Draw it all on paper first.
Array or objects: If you want to keep it simple, you can do the whole project just using arrays. You can have one array that stores all the animals. You can have a different array that stores which animals are selected right NOW (use .includes() to test if an animal is in that array). You can have another array that stores the rows that have a rank selected. When the number of elements in that row === the number of rows (is that the same as the number of animals? If yes, then you can use the length of the animals array for that test)
How do you know if the rows with a rank selected are unique? One way is to DISALLOW selected a rank that has already been selected. Again, use .includes() (e.g. arrSelectedRanks.includes(num)) to check if a rank has already been selected.
SO what do one of these checks look like?
const handleAnimalSelect = (animal) => {
const err = this.state.selectedAnimals.includes(animal);
if (err === true){
this.setState(error: animal);
}
}
return (
<input
type="radio"
name={this.props.animalName}
value={i}
onClick={this.handleAnimalSelect}
/>
{ error !== undefined && (
<div class={style.errorMessage}>{`Animal ${} was chosen twice`}</div>
)}
);
};
Remember: State is what you use for remembering the value of variables in a given component. Every component has its own state. But Props are for data/functions/elements that are passed into the component. You don't update the values of props in the component (prop values are stored in another component. If you need to update them, you use functions to pass data back to the parent component where that variable is in state, and update the value there).
Here is an example that uses .includes() to check for the presence/absence of something:
https://stackoverflow.com/a/64486351/1447509

Parsing error: Unexpected token when adding a conditional in react

Hi I'm a beginner in react, I was getting an error for scenarios when no props get passed to my component, it said something like "you can't do a map of undefined" so I added this if statement. But now I get a parsing error on line 4. Not sure' what's wrong with my code:
import React from 'react';
export const Record = props => (
if (props) {
<div className='card-container'>
{props.rows.drivingInstructions.map((val, ind) => {
return (
<tr>
<td>{ind + 1}</td>
<td>Smith</td>
<td>50</td>
</tr>
)
})}
</div>
}
{ console.log(props.rows.drivingInstructions) }
);
if can't exist in an expression context. For similar reasons, you can't do:
const someVar = if (true) 10 else null;
The syntax is not permitted. To do something like this, you need the conditional operator:
export const Record = props => (
!props.rows || !props.rows.drivingInstructions ? null : (
<div className='card-container'>
{props.rows.drivingInstructions.map((val, ind) => {
...
Or, if you're OK with the .card-container existing even if it doesn't have any rows, use optional chaining instead:
export const Record = props => (
<div className='card-container'>
{props.rows.drivingInstructions?.map((val, ind) => {
...
I would rewrite those code as below.
import React from 'react';
export const Record = (props) => {
if (!!props?.rows?.drivingInstructions?.length) {
return (<div className='card-container'>
{props.rows.drivingInstructions.map((val, ind) =>(
<tr>
<td>{ind + 1}</td>
<td>Smith</td>
<td>50</td>
</tr>
)
)}
</div>);
}
return <div>No Driving Instructions</div>;
}
};
This !!props?.rows?.drivingInstructions?.length is an optional chaining and it checks that those properties are not nullish. Lastly, it ensures the length is also more than 0 by converting to boolean using !!.
import React from 'react';
export const Record = props => (
props?.rows?.drivingInstructions?.length ?
(<div className='card-container'>
{props.rows.drivingInstructions.map((val, ind) =>
<tr>
<td>{ind + 1}</td>
<td>Smith</td>
<td>50</td>
</tr>
)}
</div>) : null;
);

Categories

Resources