Map through an array and render a specified component in React - javascript

I have 2 almost identical components that map through an array of options and generate either checkboxes or radio buttons. The only difference between the components is the child component that is rendered (either a checkbox or a radio input).
I would like to reduce the duplicate code in these components but I'm unsure as to the best way to tackle it. I could consolidate the 2 components into a single component such as FormControlGroup and add more props to allow me to select if I want either checkboxes or radio buttons to be rendered e.g. checkboxOrRadio, but this would mean that if I added new variations of checkboxes or radio buttons such as CustomCheckbox I would have to keep expanding on the props of this component.
<FormControlGroup
label="Radio buttons"
name="test"
options=[{label: 'one', value: 1}, {label: 'two', value: 2}, {label: 'three', value: 3}]
checkboxOrRadio="radio"
/>
Is it possible (and sensible) to pass a component in as a prop and have the component rendered in the map function? If so, how would I do this? Or are there any more elegant solutions? That way I could pass in any component I want and it will be rendered and have the key, name, label, onChange, value, checked props passed to it.
CheckboxGroup.js
import React from 'react';
import CheckboxInput from './CheckboxInput';
const CheckboxGroup = ({ label, name, options, onChange, children }) => {
return (
<div className="form-group form-inline">
<span className="faux-label">{label}</span>
{children}
<div className="form-inline__field-container">
{options &&
options.map(option => (
<CheckboxInput
key={option.value}
name={name}
label={option.label}
onChange={onChange}
value={option.value}
checked={option.value}
/>
))
}
</div>
</div>
);
};
export default CheckboxGroup;
RadioGroup.js
import React from 'react';
import RadioInput from './RadioInput';
const RadioGroup = ({ label, name, options, onChange, children }) => {
return (
<div className="form-group form-inline">
<span className="faux-label">{label}</span>
{children}
<div className="form-inline__field-container">
{options &&
options.map(option => (
<RadioInput
key={option.value}
name={name}
label={option.label}
onChange={onChange}
value={option.value}
checked={option.value}
/>
))
}
</div>
</div>
);
};
export default RadioGroup;

It is absolutely sensible in certain cases to pass a component as a prop. In fact, it is a pattern used in many React libraries, including React Router, which allows you to pass a component prop to the Route component.
In your case, the render function of your FormControlGroup component might look something like this:
render({component, ...}) { // component is a prop
const InputComponent = component;
return (
... // outer divs
{ options.map(option =>
<InputComponent key={option.value} ... />
}
...
)
}
Then you would use it like this:
<FormControlGroup
label="Radio buttons"
name="test"
options=[{label: 'one', value: 1}, {label: 'two', value: 2}, {label: 'three', value: 3}]
component={CheckboxInput}
/>
Another option would be to create a new component that takes care of rendering the outer <div> elements and then map options to a list of input components in some outer component. That allows you to make fewer assumptions about what props any given input component should be expecting. Since you're already using children, you'd have to split it into two components. Here's one possibility for what that might look like:
const FormControlGroup = ({ label, children }) => {
return (
<div className="form-group form-inline">
<span className="faux-label">{label}</span>
{children}
</div>
);
};
const FormControlOptions = ({ children }) => {
return (
<div className="form-inline__field-container">
{children}
</div>
);
};
const SomeOuterComponent = ({ label, name, options, onChange }) => {
<FormControlGroup label={label}>
... // other children
<FormControlOptions>
{
options.map(option =>
<CheckboxInput
key={option.value}
name={name}
label={option.label}
onChange={onChange}
value={option.value}
checked={option.value}
/>
)
}
</FormControlOptions>
</FormControlGroup>
}
Of course, there are many ways to design this. The exact implementation you go with will depend on your use case.

Doesn't seem like a bad idea to me. It's definitely possible, maybe it's not the most elegant solution, but I don't currently see what's wrong with it.

I personally wouldn't change it into a FormControlGroup component because it makes it simpler in the code where you're using the component. So you can quickly know what is a CheckboxGroup vs RadioGroup.
Changing it would mean checking the checkboxOrRadio prop every time to see exactly what component you were changing.
I'd say wait, if you start to get more elements you want the exact same way then create a common component. But for just two components to share some code it isn't a big deal.

Related

When should I put state in parent?

This is my first StackOverflow question, feel free to add any suggestion you want.
I have an AudioControls component that renders four different Controls components, each for a different audio option (volume, bass, mid, treble).
I wrote it in a way that the parent knows and manage the four of the states:
useState({
volume: 50,
bass: 50,
mid: 50,
treble: 50,
})
and then passed these values to children as props.
I have two concerns regarding this:
Control components are simply the same and maybe they could have their own value state (put an audioValue state on each Control). I've read it's a good practice to always lift the state when possible, but why is it needed? What are good reasons to do it?
Is there a way to use the map function with these Controls given that they are all pretty similar? Without losing legibility, of course.
This is the AudioControl code:
function AudioControls() {
const [audioValues, setAudioValues] = useState({
volume: 50,
bass: 50,
mid: 50,
treble: 50
})
function handleControl(option, id) {
const oldValue = audioValues[id]
const newValue = option === "+" ? oldValue + 1 : oldValue - 1
setAudioValues(prevState => (
{
...prevState,
[`${id}`] : newValue
}
))
}
return(
<>
<Control
id="volume"
name="Volume"
value={audioValues.volume}
handleControl={handleControl}
/>
<Control
id="bass"
name="Bass"
value={audioValues.bass}
handleControl={handleControl}
/>
<Control
id="mid"
name="Mid"
value={audioValues.mid}
handleControl={handleControl}
/>
<Control
id="treble"
name="Treble"
value={audioValues.treble}
handleControl={handleControl}
/>
</>
)
}
And here's the Control component:
function Control({ id, name, value, handleControl }) {
return(
<div>
<button onClick={() => handleControl("+", id)}>
+
</button>
<label>{name}: {value}</label>
<button onClick={() => handleControl("-", id)}>
-
</button>
</div>
)
}
This is my second stack overflow answer. Also open to any and all feedback.
I'd be curious to learn where you heard the fact that it's always a good idea to lift state. My thought on that is: if it's always a good idea to lift state, when do you stop lifting? Why just move state up one in this case? Why not lift it even further?
To answer your first question: the main reason to lift state into a parent component is if you need two sibling components to use the same piece of state. For example:
const [volume, setVolume] = setState(50);
function changeVolume (newVolume) {
setVolume(newVolume);
}
<VolumeDisplay volume={volume} />
<VolumeControl onChange={changeVolume} />
In your case, it looks like each one is capable of changing its own state so it's preferred to push state down to each <Control>. Once you put the audio value locally inside the control component you could map them easily like this:
const controls = [
{id: "bass", name: "Bass"},
{id: "volume", name: "Volume"},
{id: "mid", name: "Mid"},
{id: "treble", name: "Treble"},
]
return (
<>
{controls.map(({id, name}) => <Control id={id} name={name} />)}
<>
)

Send selected options from react dual listbox with post request

I'm trying to implement in my react app, two react double listbox in my component. At the moment the listboxes are filled automatically after a get request when component mounts. I need some help on how to get the selected options in each double listbox and send them to the server as json data.
I need two arrays from these lists.
This is my dual listbox classes:
import React from 'react';
import DualListBox from 'react-dual-listbox';
import 'react-dual-listbox/lib/react-dual-listbox.css';
import 'font-awesome/css/font-awesome.min.css';
export class FirstList extends React.Component {
state = {
selected: [],
};
onChange = (selected) => {
this.setState({ selected });
};
render() {
const { selected } = this.state;
return (
<DualListBox
canFilter
filterPlaceholder={this.props.placeholder || 'Search From List 1...'}
options={this.props.options}
selected={selected}
onChange={this.onChange}
/>
);
}
}
export class SecondList extends React.Component {
state = {
selected: [],
};
onChange = (selected) => {
this.setState({ selected });
};
render() {
const { selected } = this.state;
return (
<DualListBox
canFilter
filterPlaceholder={this.props.placeholder || 'Search From List 2...'}
options={this.props.options}
selected={selected}
onChange={this.onChange}
/>
);
}
}
In my component I started importing this:
import React, { useState, useEffect } from 'react'
import LoadingSpinner from '../shared/ui-elements/LoadingSpinner';
import ErrorModal from '../shared/ui-elements/ErrorModal';
import { FirstList, SecondList } from '../shared/formElements/DualListBox';
import { useHttpClient } from '../shared/hooks/http-hook';
const MyComponent = () => {
const { isLoading, error, sendRequest, clearError } = useHttpClient();
const [loadedRecords, setLoadedRecords] = useState();
useEffect(() => {
const fetchRecords = async () => {
try {
const responseData = await sendRequest(
process.env.REACT_APP_BACKEND_URL + '/components/get'
);
setLoadedRecords(responseData)
} catch (err) { }
};
fetchRecords();
}, [sendRequest]);
...
...
return (
<React.Fragment>
<ErrorModal error={error} onClear={clearError} />
<form>
<div className="container">
<div className="row">
<div className="col-md-6">
<fieldset name="SerialField" className="border p-4">
<legend className="scheduler-border"></legend>
<div className="container">
<p>SERIALS</p>
{loadedRecords ? (
<FirstList id='Serials' options={loadedRecords.firstRecordsList} />
) : (
<div>
<label>List is loading, please wait...</label>
{isLoading && <LoadingSpinner />}
</div>
)}
</div>
</fieldset>
</div>
<div className="col-md-6">
<fieldset name="SystemsField" className="border p-4">
<legend className="scheduler-border"></legend>
<div className="container">
<p>SYSTEMS</p>
{loadedRecords ? (
<SecondList options={loadedRecords.secondRecordsList} />
) : (
<div>
<label>List is loading, please wait...</label>
{isLoading && <LoadingSpinner />}
</div>
)}
</div>
</fieldset>
</div>
...
...
If anyone could guide me it'll be much appreciated.
Thanks in advance!
FirstList and SecondList are using internal state to show the selected values. Since a parent component should do the server request, it needs access to this data. This can be achieved by a variety of options:
Let the parent component (MyComponent) handle the state completely. FirstList and SecondList would need two props: One for the currently selected values and another for the onChange event. MyComponent needs to manage that state. For example:
const MyComponent = () => {
const [firstListSelected, setFirstListSelected] = useState();
const [secondListSelected, setSecondListSelected] = useState();
...
return (
...
<FirstList options={...} selected={firstListSelected} onChange={setFirstListSelected} />
...
<SecondList options={...} selected={secondListSelected} onChange={setSecondListSelected} />
...
)
Provide only the onChange event and keep track of it. This would be very similar to the first approach, but the lists would keep managing their state internally and only notify the parent when a change happens through onChange. I usually don't use that approach since it feels like I'm managing the state of something twice and I also need to know the initial state of the two *List components to make sure I am always synchronized properly.
Use a ref, call an imperative handle when needed from the parent. I wouldn't recommend this as it's usually not done like this and it's getting harder to share the state somewhere else than inside of the then heavily coupled components.
Use an external, shared state like Redux or Unstated. With global state, the current state can be reused anywhere in the Application and it might even exist when the user clicks away / unmounts MyComponent. Additional server requests wouldn't be necessary if the user navigated away and came back to the component. Anyways, using an external global state needs additional setup and usually feels "too much" and like a very high-end solution that is probably not necessary in this specific case.
By using option 1 or 2 there is a notification for the parent component when something changed. On every change a server request could be sent (might even be debounced). Or there could be a Submit button which has a callback that sends the saved state to the server.

Get value from an input in an array on button click ReactJs

I am relatively new to ReactJs.I am learning react while I am trying to create a real world app. Here is something I cannot solve.
I have a repeated component that has one input and one button.
everytime the button is clicked, the value of the input will be used in one function.
In Angular I do not have to worry about how to passing those value since in ngFor we can directly assign the value from the ngModel. But there is no such concept in React.
betOnTeam = (_id, coins) => {
return;
};
{this.teamList.map(team => (
<div key={team._id}>
<input type="number" min="100" max="5000" />
<button type="button"
onClick={() => this.betOnTeam(team._id,//value from the
input above)}>
</div>
))}
So basically I Have a function ready to receive an Id and how many coins the user bet.
And As we can see from the picture, I have many inputs which should contain the value of how much coins the user put for a specific team.
each button will trigger this betOnTeam function and will pass the unique Id of the team, and the number coins the user bet.
How can I set states for all thoese teams since they are all dynamic, it could be 5 teams or 100 teams. Is it any way to do it dynamically?
e.g. user input 5000, when he click the button, the id and the value will be passed into the function betOnTeam.
I hope this clarified my question.
==================================
Thanks for all the input from you guys.
I have make it working combine with all your suggestions.
So Here is what I do:
betOnTeam = (event, id) => {
console.log(event.target[0].value, id);
return;
};
{this.teamList.map(team => (
<form key={team._id} onSubmit={(e) => this.betOnTeam(e,team._id)}>
<input type="number" min="100" max="5000" />
<button type="submit">
</form >
))}
Seems like you're really close. I think this ultimately comes down to how you want to construct your components. There is an easy way to do this (the more React) way, and there is a hard way.
The easy way is to split the mark-up created inside the .map() into its own component. You will have an individual component for each team, thus the state is encapsulated to its own component. By doing this you can effectively keep track of the inputs for each team.
Consider this sandbox for example: https://codesandbox.io/s/dazzling-roentgen-jp8zm
We can create a component for the markup like this:
Team
import React from "react"
class Team extends React.Component {
state = {
betValue: 100
};
handleOnChange = e => {
this.setState({
betValue: e.target.value
});
};
handleOnClick = () => {
this.props.betOnTeam(this.state.betValue, this.props.id);
};
render() {
const { id } = this.props;
const { betValue } = this.state;
return (
<div key={id}>
<input
type="number"
min="100"
max="5000"
value={betValue}
onChange={this.handleOnChange}
/>
<button onClick={this.handleOnClick} type="button">
Bet
</button>
</div>
);
}
}
export default Team;
So from a purely jsx standpoint, the markup is the same, but now it is contained inside a class-component.
Now we can keep track of the inputs in a controlled manner.
When we're ready to place the bet, the value is stored in the
individual component state.
We pass down two properties to each Team component, the team_id and
betOnTeam function. The team_id can be accessed using this.props.id and likewise we will pass it into this.props.betOnTeam() when required.
Main Component
import React from "react"
import Team from "./Team"
class App extends React.Component {
teamList = [
{ team_id: 1, name: "TSM" },
{ team_id: 2, name: "SKT" },
{ team_id: 3, name: "CLG" }
];
betOnTeam = (betValue, teamId) => {
console.log(betValue);
console.log(teamId);
};
render() {
return (
<div>
{this.teamList.map(team => (
<Team id={team.team_id} betOnTeam={this.betOnTeam} />
))}
</div>
);
}
}
So the .map() renders a Team component for each team and passes in their respective ids and the betOnTeam function as props. When the button inside the component is clicked, we can pass back up the values stored in the Team Component to execute betOnTeam.
onClick={this.betOnTeam(form._id,value)}
Don't execute this.betOnTeam right from the start, you're actually setting the click handler to the returned result of this.betOnTeam(form._id,value). In React, it's not quite the same as in Angular. For this, you need to set it equal to a function that does that. Like this:
onClick={() => this.betOnTeam(form._id,value)}
Hope this helps.
1. this.betOnTeam = (_id, value) => { ... }
2. constructor(props) { this.betOnTeam.bind(this) }
3. onClick = { () => this.betOnTeam(form._id, value)}
Well if you use onClick = { this.betOnTeam(form._id, value) }, then the code will be executed first, and in betOnTeam function, you will not use 'this' operator.
But if you use the above methods, then you can use 'this' in the function and get the good results.
And your code has some bugs to fix,
{this.array.map(form => (
<div key={form._id}>
<input name={`editform{form._id}`} type="number" min="100" max="5000" onChange={(e) => this.changeNumber(e, form._id) }/>
<button type="button"
onClick={this.betOnTeam(form._id,value)}>
</div>
))}
And in changeNumber function, you should use setState({}) function to set the value to the state, and in betOnTeam function, you can use the state you have already set.
The code must be like this, or otherwise you can use ref but it is not formally encouraged to use ref.
Totally, you should use ControlledComponent. That's the target.
I hope you to solve the problem.

How to loop over values not part of same array in JSX

I am making a component which is fairly simple as far as layout and design goes, it has 3 categories and data associated with it, the layout is pretty much like this:
<div>
<strong>User Name: </strong>
{userName}
</div>
<div>
<strong>User ID: </strong>
{userID}
</div>
<div>
<strong>Hobbies: </strong>
{comma-separated-list}
</div>
Currently all the data is coming from different parts so is not part of a single array or object, and so I can't use .map. The categories are all just constant string values. What is the best way to construct a small component that I can re-use for displaying the data in loop here. Since its pretty much repetition of a div tag with just different data, I would like to reuse the div and just pass the static category names and data associated with those categories.
Write a simple stateless functional component and pass the required data into props.
Like this:
const DisplayCaterogy = ({ categoryName, categoryValue}) => (
<div>
<strong>{categoryName}: </strong>
{categoryValue}
</div>
)
Now you need to pass categoryName and categoryValue to component to render the data in dom.
Like this:
<DisplayCaterogy categoryName="User Name" categoryValue={/* name of the user */} />
Note: Don't forgot to export/import the component, if you define that in a separate file.
You can have something like this:
Data:
data: [
{
name: 'Username',
value: 'Name',
},
{
name: 'Hobbies',
value: ['one', 'two']
}
]
And the component:
function ListCategory({ data }) {
return data.map(category =>
<Category name={category.name} value={category.value} />
);
}
function Category({ name, value }) {
return (
<div>
<strong>{name}: </strong>
{Array.isArray(value) ? value.join(', ') : value}
</div>
);
}
<ListCategory data={data} />

React pattern for List Editor Dialog

I'd like to know what's the best pattern to use in the following use case:
I have a list of items in my ItemList.js
const itemList = items.map((i) => <Item key={i}></Item>);
return (
<div>{itemList}</div>
)
Each of this Items has an 'EDIT' button which should open a dialog in order to edit the item.
Where should I put the Dialog code?
In my ItemList.js => making my Item.js call the props methods to open the dialog (how do let the Dialog know which Item was clicked? Maybe with Redux save the id of the item inside the STORE and fetch it from there?)
In my Item.js => in this way each item would have its own Dialog
p.s. the number of items is limited, assume it's a value between 5 and 15.
You got a plenty of options to choose from:
Using React 16 portals
This option let you render your <Dialog> anywhere you want in DOM, but still as a child in ReactDOM, thus maintaining possibility to control and easily pass props from your <EditableItem> component.
Place <Dialog> anywhere and listen for special app state property, if you use Redux for example you can create it, place actions to change it in <EditableItem> and connect.
Use react context to send actions directly to Dialog, placed on top or wherever.
Personally, i'd choose first option.
You can have your <Dialog/> as separate component inside application's components tree and let it to be displayed in a case if your application's state contains some property that will mean "we need to edit item with such id". Then into your <Item/> you can just have onClick handler that will update this property with own id, it will lead to state update and hence <Dialog/> will be shown.
UPDATED to better answer the question and more completely tackle the problem. Also, followed the suggestion by Pavlo Zhukov in the comment below: instead of using a function that returns functions, use an inline function.
I think the short answer is: The dialog code should be put alongside the list. At least, this is what makes sense to me. It doesn't sound good to put one dialog inside each item.
If you want to have a single Dialog component, you can do something like:
import React, { useState } from "react";
import "./styles.css";
const items = [
{ _id: "1", text: "first item" },
{ _id: "2", text: "second item" },
{ _id: "3", text: "third item" },
{ _id: "4", text: "fourth item" }
];
const Item = ({ data, onEdit, key }) => {
return (
<div key={key}>
{" "}
{data._id}. {data.text}{" "}
<button type="button" onClick={onEdit}>
edit
</button>
</div>
);
};
const Dialog = ({ open, item, onClose }) => {
return (
<div>
<div> Dialog state: {open ? "opened" : "closed"} </div>
<div> Dialog item: {JSON.stringify(item)} </div>
{open && (
<button type="button" onClick={onClose}>
Close dialog
</button>
)}
</div>
);
};
export default function App() {
const [isDialogOpen, setDialogOpen] = useState(false);
const [selectedItem, setSelectedItem] = useState(null);
const openEditDialog = (item) => {
setSelectedItem(item);
setDialogOpen(true);
};
const closeEditDialog = () => {
setDialogOpen(false);
setSelectedItem(null);
};
const itemList = items.map((i) => (
<Item key={i._id} onEdit={() => openEditDialog(i)} data={i} />
));
return (
<>
{itemList}
<br />
<br />
<Dialog
open={isDialogOpen}
item={selectedItem}
onClose={closeEditDialog}
/>
</>
);
}
(or check it directly on this CodeSandbox)

Categories