React/Redux - save select value onChange - javascript

I've created a Component that lets you Add/Remove a dropdown fields (onClick of two buttons). I'm using Redux to keep the state of the dropdown so that when I navigate back and forth (React Router) the added dropdowns are not lost.
Now I also need to keep the values of each dropdown onChange. So onChange I'm sending the value of the dropdowns to the reducer but it seems to be updating all the dropdowns at once (not individually).
Any idea how to fix this?
Here is my code so far:
Component
import React from 'react'
import { connect } from 'react-redux'
import uuidV4 from 'uuid-v4'
import { saveSelect, removeSelect, saveSelectValue, incrementCounter, decrementCounter } from '../actions.js'
class AccountUpgrade extends React.Component {
constructor(props) {
super(props);
}
addInput = (counter) => {
this.props.saveSelect(uuidV4())
this.props.incrementCounter(counter)
}
removeInput = (index, counter) => {
this.props.removeSelect(index)
this.props.decrementCounter(counter)
}
saveSelectValue = (e) => {
let data = {}
data = e.target.value
this.props.saveSelectValue(data)
}
renderSelect = (id, index) => {
const selectedValue = this.props.selectedValue || ''
const counter = this.props.counter
return(
<div className="col-12">
<select
key={id}
name={'document-'+ id}
value={selectedValue}
onChange = {this.saveSelectValue}
>
<option value="0">Please Select</option>
<option value="1">Australia</option>
<option value="2">France</option>
<option value="3">United Kingdom</option>
<option value="4">United States</option>
</select>
<button onClick={ () => {this.removeInput(index, counter) }}>Remove</button>
</div>
)
}
render(){
const ids = this.props.ids || []
const counter = this.props.counter
return (
<div>
{this.props.counter <= 4 &&
<button onClick={ this.addInput }>Add</button>
}
<div className="inputs">
{ids.map(this.renderSelect)}
</div>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
store: state.EligibleAbout,
ids: state.EligibleAbout.ids,
selectedValue: state.EligibleAbout.selectedValue,
counter: state.EligibleAbout.counter,
}
}
const EligibleAbout = connect(mapStateToProps, {saveSelect, removeSelect, saveSelectValue, incrementCounter, decrementCounter})(AccountUpgrade)
export default EligibleAbout
action.js
export const ADD_SELECT = 'ADD_SELECT'
export const REMOVE_SELECT = 'REMOVE_SELECT'
export const SAVE_SELECT_OPTION = 'SAVE_SELECT_OPTION'
export const INCREMENT_COUNTER = 'INCREMENT_COUNTER'
export const DECREMENT_COUNTER = 'DECREMENT_COUNTER'
export function saveSelect(data) {
return { type: ADD_SELECT, data }
}
export function removeSelect(data) {
return { type: REMOVE_SELECT, data }
}
export function saveSelectValue(data) {
return { type: SAVE_SELECT_OPTION, data}
}
export function incrementCounter(data) {
return { type: INCREMENT_COUNTER, data }
}
export function decrementCounter(data) {
return { type: DECREMENT_COUNTER, data }
}
reducer.js
import { combineReducers } from 'redux'
import { ADD_SELECT, REMOVE_SELECT, SAVE_SELECT_OPTION } from './actions'
function EligibleAbout(state = { ids: [], counter: 0, selectedValue: 'Please select'}, action = {}){
switch (action.type){
case ADD_SELECT:
return {
...state,
ids: [].concat(state.ids, action.data),
}
case REMOVE_SELECT:
return {
...state,
ids: state.ids.filter((id, index) => (index !== action.data)),
}
case SAVE_SELECT_OPTION:
return {
...state,
selectedValue: action.data
}
case INCREMENT_COUNTER:
return {
...state,
counter: state.counter + 1
}
case DECREMENT_COUNTER:
return {
...state,
counter: state.counter - 1
}
default:
return state
}
}
const FormApp = combineReducers({
EligibleAbout
})
export default FormApp

Since you need to keep the selected value for each id or dropdown, the best approach would be keeping an object array with objects which have id and value property instead of having string array ids. So you will have to change your code into something like this. I didn't test the code, so let me know in comments if you get any issue with it.
class AccountUpgrade extends React.Component {
constructor(props) {
super(props);
}
addInput = () => {
this.props.saveSelect({id:uuidV4()})
}
removeInput = (index) => {
this.props.removeSelect(index)
}
saveSelectValue = (e, id) => {
let data = {}
data.id = id;
data.value = e.target.value
this.props.saveSelectValue(data)
}
renderSelect = (selection, index) => {
const selectedValue = selection.value || '';
const id = selection.id;
return(
<div className="col-12">
<select
key={id}
name={'document-'+ id}
value={selectedValue}
onChange = {(e) => this.saveSelectValue(e, id)}
>
<option value="0">Please Select</option>
<option value="1">Australia</option>
<option value="2">France</option>
<option value="3">United Kingdom</option>
<option value="4">United States</option>
</select>
<button onClick={ () => {this.removeInput(index) }}>Remove</button>
</div>
)
}
render(){
const selections = this.props.selections || []
return (
<div>
<button onClick={ this.addInput }>Add</button>
<div className="inputs">
{selections.map(this.renderSelect)}
</div>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
store: state.EligibleAbout,
selections: state.EligibleAbout.selections,
}
}
You need to modify your reducer as well.
function EligibleAbout(state = { selections: [] } , action = {}){
switch (action.type){
case ADD_SELECT:
return {
...state,
selections: [].concat(state.selections, action.data),
}
case REMOVE_SELECT:
return {
...state,
selections: state.selections.filter((selection, index) => (index !== action.data)),
}
case SAVE_SELECT_OPTION:
return {
...state,
selections: state.selections.map((selection) => selection.id === action.data.id ? action.data : selection)
}
default:
return state
}
}

You are only storing one selected value that they all read from. You'll want to give each select an ID of some sort and save / recall it's selected state specifically. Something like:
case SAVE_SELECT_OPTION:
return {
...state,
selectedValues: {
[action.selectID]: action.data
}
}
Of course you'll need to update your dispatch and actions accordingly.

Related

Why changes from local state don't go into global state?

To-Do-List
When I try to edit my created task, I see some modifications, but only in local State. When I look at the data of the global state, nothing change, the data remains the same as after creating the tasks object.
It is also interesting to note that when case EDIT_TASK has worked , action.id = values from Input, and action.task = undefined
P.S: Put all the component code below, maybe there was a mistake somewhere.
P.S: Sorry for ENG
Component's code
import React from 'react'
import s from "./../../App.module.css";
class Item extends React.Component {
state = {
statusChange: false,
task: this.props.task
}
activeStatusChange = () => {
this.setState( {
statusChange: true
}
);
}
deActivateStatusChange = () => {
this.setState( {
statusChange: false
}
);
this.props.editTask(this.props.task)
}
onStatusChange = (e) => {
this.setState({
task: e.target.value
})
}
render(){
return (
<div className={s.item}>
<span onClick={this.props.editStatus} className={s.statusTask}>
{this.props.status ? <img src="https://img.icons8.com/doodle/48/000000/checkmark.png"/>
: <img src="https://img.icons8.com/emoji/48/000000/red-circle-emoji.png"/>}
</span>
{ this.state.statusChange
? <input onChange={this.onStatusChange} autoFocus={true} onBlur={this.deActivateStatusChange} value={this.state.task} />
: <span className={this.props.status === true ? s.task : s.taskFalse} onClick={this.activeStatusChange}> {this.state.task} </span>}
<span onClick={this.props.deleteTask} className={s.close}><img src="https://img.icons8.com/color/48/000000/close-window.png"/></span>
</div>
)
}
}
export default Item;
Reducer's code
import React from 'react'
import shortid from 'shortid';
const ADD_TASK = 'ADD_TASK'
const EDIT_TASK = 'EDIT_TASK'
const initialState = {
tasks: []
};
const mainReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TASK: {
return {
...state,
tasks: [{
id: shortid.generate(),
task: action.task,
status: false
}, ...state.tasks]
}
}
case EDIT_TASK: {
return {
...state,
tasks: state.tasks.filter((t) => t.id === action.id ? {...t, task: action.newTask} : t)
}
}
default:
return state
}
}
//window.store.getState().mainReducer.tasks
export const addTask = task => ({type: 'ADD_TASK', task});
export const editTask = (id,newTask) => ({type: 'EDIT_TASK', id, newTask})
export default mainReducer;
Parent's component:
import React from "react";
import s from "./../../App.module.css";
import CurrentTasks from "../current-tasks";
import FilterButtonTasks from "../filter-button-tasks";
import ListTasks from "../tasks-list";
class SetForm extends React.Component {
constructor(props) {
super(props);
this.state = {
text: ''
}
}
onInputChange = event => {
this.setState({
[event.target.name]: event.target.value
})
}
handleSubmit = event => {
event.preventDefault();
if(this.state.text === '') {
return undefined
}
this.props.addTask(this.state.text)
this.setState({
text: ''
})
}
filterTasks = (tasks, activeFilter) => {
switch (activeFilter) {
case 'done': {
return tasks.filter(task => task.status);
}
case 'active': {
return tasks.filter(task => !task.status)
}
default:
return tasks;
}
}
render() {
const currentTasks = this.filterTasks(this.props.tasks, this.props.filter);
return (
<div>
<form onSubmit={this.handleSubmit}>
<div>
<input name={"text"} onChange={this.onInputChange} value={this.state.text}placeholder={"Set your task"} className={s.setTask}/>
<button onClick={this.handleSubmit} className={s.add}>ADD</button>
<button onClick={this.props.removeAllTasks} className={s.clearAll}>Clear</button>
</div>
</form>
<CurrentTasks tasks={this.props.tasks}/>
<ListTasks currentTasks={currentTasks} editStatus={this.props.editStatus} deleteTask={this.props.deleteTask} editTask={this.props.editTask}/>
<FilterButtonTasks currentTasks={currentTasks} changeFilter={this.props.changeFilter} removeAllDone={this.props.removeAllDone}/>
</div>
)
}
}
export default SetForm;
one more:
import React from 'react'
import Item from './SetItem/item'
const ListTasks = ({currentTasks,editStatus,deleteTask,editTask}) => {
return (
currentTasks.map(t => (<Item editStatus={() => editStatus(t.id)}
deleteTask={() => deleteTask(t.id)}
key={t.id} task={t.task} status={t.status} editTask={editTask}/>))
)
}
export default ListTasks;
Since, you are only updating the local state onStatusChange the state does not get updated in global state. So on deActivateStatusChange you need to call this.props.editTask with updated state, that is this.state.task
deActivateStatusChange = () => {
this.setState({
statusChange: false
});
this.props.editTask(this.state.task); // change is here
};
The problem is in your EDIT_TASK reducer:
Change
state.tasks.filter((t) => t.id === action.id ? {...t, task: action.newTask} : t)
To
state.tasks.map((t) => t.id === action.id ? {...t, task: action.newTask} : t)
map will update the object, not filter
Code should be:
case EDIT_TASK: {
return {
...state,
tasks: state.tasks.map((t) => t.id === action.id ? {...t, task: action.newTask} : t)
}
}
Also it seems like you are not passing id and newTask to editTask action:
const ListTasks = ({ currentTasks, editStatus, deleteTask, editTask }) => {
return currentTasks.map(t => (
<Item
editStatus={() => editStatus(t.id)}
deleteTask={() => deleteTask(t.id)}
key={t.id}
task={t.task}
status={t.status}
editTask={(newTask) => editTask(t.id, newTask)} // change current code to this
/>
));
};

In React, How to update state between multiple select dropdowns , each with same options?

I have a problem where I have to render 4 dropdowns, each with similar options(i just rendered 1 here). If I select an option in any one of the dropdowns, that option should not be available in the other three.
How should I update the selected_planets in the state? The below code updates the selected_planets from 1 select dropdown. But still that same option is available everywhere and I could not able to get 4 different options inside selected_planets array? How should I proceed?
Also, the response from API fetch is an array of objects, which I mapped through and update in planets Array. For demo purpose, let's consider, planets: [Neptune, Saturn, Mars, Earth, Venus, Jupiter]
import React, { Component } from 'react';
export default class Dashboard extends Component {
state = {
planets: [],
selected_planets: []
};
componentDidMount() {
fetch('url')
.then(response => {
return response.json();
})
.then(data => {
this.setState({ planets: data });
});
}
handleSelect = event => {
this.setState({ selected_planets: [event.target.value] });
};
render() {
let select_Planets = this.state.planets.map(planet => {
return planet.name;
});
let options = select_Planets.map(planet => (
<option key={planet} value={planet}>
{planet}
</option>
));
return (
<select onChange={this.handleSelect}>
<option defaultChecked></option>
{options}
</select>
);
}
}
This can be achieved by producing a new set of options for each dropdown on render based off of what are the currently selected options, and what is the selected option for that dropdown.
First make sure each dropdown is binding to a property in your component's state and updating on change:
constructor() {
super();
this.state = {
planets: ["a", "b", "c", "d"],
inputs: {
d1: "",
d2: ""
}
};
this.handleChange = this.handleChange.bind(this);
}
handleChange({ target }) {
this.setState({
...this.state,
inputs: {
...this.state.inputs,
[target.name]: target.value
}
});
}
<select
name="d1"
value={this.state.inputs.d1}
onChange={this.handleChange}>
Then you can obtain a list of the selected planets within the render method by converting the input object into an array using Object.values():
const selectedPlanets = Object.values(this.state.inputs);
Then create a new array for each of the dropdowns which will omit any planets which have already been selected unless it is selected by that particular dropdown itself:
const d1Options = this.state.planets.filter(
p => !selectedPlanets.find(sP => sP === p) || p === this.state.inputs.d1
);
const d2Options = this.state.planets.filter(
p => !selectedPlanets.find(sP => sP === p) || p === this.state.inputs.d2
);
<select
name="d1"
value={this.state.inputs.d1}
onChange={this.handleChange}>
<option></option>
{d1Options.map(o => (
<option key={o}>{o}</option>
))}
</select>
I've put together a working example here:
https://codepen.io/curtis__/pen/pozmOmx
You can solve this is multiple ways. Here is pseudo-code.
Create an object or array to hold the values of selected index. I will suggest to use useState API. Instead of setState.
class extends Component {
static state = {
dropdown1: "",
dropdown2: "",
dropdown3: "",
dropdown4: ""
}
constructor(props) {
super(props)
}
handleClick = (id, {target: {value}}) => {
this.setState({
[id]: value
})
}
render() {
<div>
<select onChange={this.handleClick.bind(null, "dropdown1")}>
</select>
<select onChange={this.handleClick.bind(null, "dropdown2")}>
</select>
<select onChange={this.handleClick.bind(null, "dropdown3")}>
</select>
<select onChange={this.handleClick.bind(null, "dropdown4")}>
</select>
</div>
}
}
You should replace this line
let select_Planets = this.state.planets.map(planet => {
return planet.name;
});
with
let select_Planets = this.state.planets.filter(planet => !this.state.selected_planets.includes(planet))
This will make sure that the only available options are those that have not been selected already. you can do it for all the other dropdowns.
you also replace the following lines
handleSelect = event => {
this.setState({ selected_planets: [event.target.value] });
};
with
handleSelect = event => {
this.setState({ selected_planets: [...this.state.selected_planets, event.target.value] });
};
You can manage all value in a Mother component, passing the selected option to all child components.
function DropdownBoxes({ url }) {
const [options, setOptions] = useState([]);
const [selected, setSelected] = useState({ a: 'ABC', b: 'CDE' });
useEffect(() => { // componentDidMount
fetch(url).then(setOptions);
}, [url]);
const onChange = (key, value) => {
setSelected(prev => ({ // this.setState
...prev,
[key]: value,
}));
}
const filterOptions = Object.values(selected); // ['ABC', 'CDE']
return (
<div>
<DropdownBox
options={options}
filterOptions={filterOptions}
value={selected['a']}
onChange={val => onChange('a', val)}
/>
<DropdownBox
options={options}
filterOptions={selected}
value={selected['b']}
onChange={val => onChange('b', val)}
/>
</div>
)
}
When you render the options, add a filter to show the option only if it is equal to the value, or it is not a subset of filterOptions. If you cannot/ do not want to chnage any code of dropdown box, you can add the filter to mother component when to passing options.
function DropdownBox({options, filterOptions, value, onChange}) {
...
const filter = (opt) => {
return opt.value === value || !filterOptions.includes(opt.value);
}
return (
<select>
{options.filter(filter).map(opt => (
<option value={opt.value} ...>{opt.label}</option>
))}
</select>
)
}
You can create a SelectDropDown view component and render it in the parent component. And the selected value for all dropdown is maintained by its parent component eg: selectedPlanets state.
// structure for selectedPlanets state.
type SelectedPlanets = {
[dropDownId: string]: string
};
Rusable SelectDropDown.js
import * as React from 'react';
type Props = {
valueField: string,
primaryKeyField: string, // field like id or value.
selectedValues: Array<string>, // values of the primaryField attribute that uniquely define the objects like id, or value
options: Array<Object>,
handleSelect: (event: SystheticEvent<>) => void
}
export default class SelectDropDown extends React.Component<Props> {
render() {
const {
options,
selectedValues,
primaryKeyField,
valueField,
handleSelect
} = this.props;
const optionsDom = options.map(option => {
if(!selectedValues.includes(option[primaryKeyField])){
return (
<option key={option[primaryKeyField]} value={option[valueField]}>
{planet}
</option>
);
}
});
return (
<select onChange={handleSelect}>
<option defaultChecked></option>
{optionsDom}
</select>
);
}
}
Sudo code Dashboard.js
import * as React from 'react';
import SelectDropDown from "./SelectDropDown"
export default class Dashboard extends React.Component {
state = {
planets: [],
selectedPlanets: {}
};
/*
Assumimg planets struct
[{
planetId: 1,
planet: "Earth"
}]
*/
componentDidMount() {
fetch('url')
.then(response => {
return response.json();
})
.then(data => {
this.setState({ planets: data });
});
}
handleSelect = (dropDownId, event) => {
const { selectedPlanets } = this.state;
const selectedPlanetsCopy = {...selectedPlanets};
selectedPlanetsCopy[dropDownId] = event.target.value;
this.setState({ selectedPlanets: selectedPlanetsCopy });
};
getSelectedValues = (dropDownId) => {
const {selectedPlanets} = this.state;
const selectedValues = [];
Object.keys(selectedPlanets).forEach((selectedPlanetDropDownId) => {
if(dropDownId !== selectedPlanetDropDownId) {
selectedValues.push(selectedPlanets[selectedPlanetDropDownId]);
}
});
return selectedValues;
}
render() {
const { planets } = this.state;
return (
<SelectDropDown
valueField={"planet"}
primaryKeyField={"planetId"}
selectedValues={this.getSelectedValues("dropDown1")}
options={planets}
handleSelect={this.handleSelect.bind(this, "dropDown1")}
/>
<SelectDropDown
valueField={"planet"}
primaryKeyField={"planetId"}
selectedValues={this.getSelectedValues("dropDown2")}
options={planets}
handleSelect={this.handleSelect.bind(this, "dropDown2")}
/>
);
}
}

How to target specific element after mapping and passing onClick function as props

I am facing such problem, i got my array of records fetched from an API, mapped it into single elements and outputting them as single components. I have function which changes state of parent Component, passes value to child component and child component should hide/show div content after button is clicked.
Of course. It is working, but partially - my all divs are being hidden/shown. I have set specific key to each child component but it doesn't work.
App.js
import React, { Component } from 'react';
import './App.css';
import axios from 'axios';
import countries from '../../countriesList';
import CitySearchForm from './CitySearchForm/CitySearchForm';
import CityOutput from './CityOutput/CityOutput';
import ErrorMessage from './ErrorMessage/ErrorMessage';
class App extends Component {
state = {
country: '',
error: false,
cities: [],
infoMessage: '',
visible: false
}
getCities = (e) => {
e.preventDefault();
const countryName = e.target.elements.country.value.charAt(0).toUpperCase() + e.target.elements.country.value.slice(1);
const countryUrl = 'https://api.openaq.org/v1/countries';
const wikiUrl ='https://en.wikipedia.org/w/api.php?action=query&prop=extracts&exintro&explaintext&format=json&category=city&redirects&origin=*&titles=';
const allowedCountries = new RegExp(/spain|germany|poland|france/, 'i');
if (allowedCountries.test(countryName)) {
axios
.get(countryUrl)
.then( response => {
const country = response.data.results.find(el => el.name === countryName);
return axios.get(`https://api.openaq.org/v1/cities?country=${country.code}&order_by=count&sort=desc&limit=10`)
})
.then( response => {
const cities = response.data.results.map(record => {
return { name: record.city };
});
cities.forEach(city => {
axios
.get(wikiUrl + city.name)
.then( response => {
let id;
for (let key in response.data.query.pages) {
id = key;
}
const description = response.data.query.pages[id].extract;
this.setState(prevState => ({
cities: [...prevState.cities, {city: `${city.name}`, description}],
infoMessage: prevState.infoMessage = ''
}))
})
})
})
.catch(error => {
console.log('oopsie, something went wrong', error)
})
} else {
this.setState(prevState => ({
infoMessage: prevState.infoMessage = 'This is demo version of our application and is working only for Spain, Poland, Germany and France',
cities: [...prevState.cities = []]
}))
}
}
descriptionTogglerHandler = () => {
this.setState((prevState) => {
return { visible: !prevState.visible};
});
};
render () {
return (
<div className="App">
<ErrorMessage error={this.state.infoMessage}/>
<div className="form-wrapper">
<CitySearchForm getCities={this.getCities} getInformation={this.getInformation} countries={countries}/>
</div>
{this.state.cities.map(({ city, description }) => (
<CityOutput
key={city}
city={city}
description={description}
show={this.state.visible}
descriptionToggler={this.descriptionTogglerHandler} />
))}
</div>
);
}
}
export default App;
CityOutput.js
import React, { Component } from 'react';
import './CityOutput.css';
class CityOutput extends Component {
render() {
const { city, descriptionToggler, description, show } = this.props;
let descriptionClasses = 'output-record description'
if (show) {
descriptionClasses = 'output-record description open';
}
return (
<div className="output">
<div className="output-record"><b>City:</b> {city}</div>
<button onClick={descriptionToggler}>Read more</button>
<div className={descriptionClasses}>{description}</div>
</div>
)
}
};
export default CityOutput;
Put the visible key and the toggle function in the CityOutput instead of having it in the parent
import React, { Component } from "react";
import "./CityOutput.css";
class CityOutput extends Component {
state = {
visible: true
};
descriptionTogglerHandler = () => {
this.setState({ visible: !this.state.visible });
};
render() {
const { city, description } = this.props;
let descriptionClasses = "output-record description";
if (this.state.visible) {
descriptionClasses = "output-record description open";
}
return (
<div className="output">
<div className="output-record">
<b>City:</b> {city}
</div>
<button onClick={() => this.descriptionTogglerHandler()}>Read more</button>
<div className={descriptionClasses}>{description}</div>
</div>
);
}
}
export default CityOutput;
There are two ways of how I would approach this,
The first one is setting in your state a key property and check and compare that key with the child keys like:
state = {
country: '',
error: false,
cities: [],
infoMessage: '',
visible: false.
currKey: 0
}
descriptionTogglerHandler = (key) => {
this.setState((prevState) => {
return { currKey: key, visible: !prevState.visible};
});
};
// then in your child component
class CityOutput extends Component {
render() {
const { city, descriptionToggler, description, show, currKey, elKey } = this.props;
let descriptionClasses = 'output-record description'
if (show && elKey === currKey) {
descriptionClasses = 'output-record description open';
}
return (
<div className="output">
<div className="output-record"><b>City:</b> {city}</div>
<button onClick={() => descriptionToggler(elKey)}>Read more</button>
<div className={descriptionClasses}>{description}</div>
</div>
)
}
};
The other way is to set an isolated state for every child component
class CityOutput extends Component {
constructor(props) {
this.state = {
show: false
}
}
function descriptionToggler() {
const {show} = this.state;
this.setState({
show: !show
})
}
render() {
const { city, descriptionToggler, description } = this.props;
let descriptionClasses = 'output-record description'
if (this.state.show) {
descriptionClasses = 'output-record description open';
}
return (
<div className="output">
<div className="output-record"><b>City:</b> {city}</div>
<button onClick={descriptionToggler}>Read more</button>
<div className={descriptionClasses}>{description}</div>
</div>
)
}
};
I hope this helps ;)

React - Multiple components with the same actions & reducers

I created a component that lets you add/remove additional dropdowns onClick of a button. I use Redux to keep the state of the added fields and value selected.
It works fine but if I add the component twice on the page (using the same actions and reducers), both dropdowns will update at the same time.
How could I make them work independently?
index.jsx
import React from 'react'
import { connect } from 'react-redux'
import DropDownField from './form/drop-down-field'
import uuidV4 from 'uuid-v4'
import { saveSelect, removeSelect, saveSelectValue } from './actions.js'
class Component extends React.Component {
constructor(props) {
super(props);
}
saveData(e) {
let data = {}
data[e.target.name] = e.target.value
this.context.store.dispatch(
addData(data)
)
}
addInput = (e) => {
e.preventDefault()
this.props.saveSelect({id:uuidV4()})
}
removeInput = (index, e) => {
e.preventDefault()
this.props.removeSelect(index)
}
saveSelectValue = (e, id) => {
let data = {}
data.id = id
data.value = e.target.value
this.props.saveSelectValue(data)
}
renderNationalitiesSelect = (selection, index) => {
const selectedValue = selection.value || ''
const id = selection.id
return(
<div>
<DropDownField
key={id}
name={'field-'+ id}
value={selectedValue}
onChange = {(e) => { this.saveSelectValue(e, id) }}
required
options={{
0: 'Please Select',
1: 'British',
2: 'French',
3: 'American',
4: 'Australian'
}} />
<a href="#" onClick={ (e) => {this.removeInput(index, e) }}>Remove</a>
</div>
)
}
renderCountriesSelect = (selection, index) => {
const selectedValue = selection.value || ''
const id = selection.id
return(
<div>
<DropDownField
key={id}
name={'field-'+ id}
value={selectedValue}
onChange = {(e) => { this.saveSelectValue(e, id) }}
required
options={{
0: 'Please Select',
1: 'United Kingdom',
2: 'France',
3: 'United States',
4: 'Australia'
}} />
<a href="#" onClick={ (e) => {this.removeInput(index, e) }}>Remove</a>
</div>
)
}
render(){
const selections = this.props.selections || []
let {
Nationality,
CountryOfResidence
} = this.props.store
return (
<DropDownField name="Nationality" value={Nationality} options={{
0: 'Please Select', 1: 'British', 2: 'French', 3: 'American', 4: 'Australian'
}} onChange={this.saveData.bind(this)} />
<div>
<div>
{selections.map(this.renderNationalitiesSelect)}
</div>
{this.props.selections.length < 4 &&
<div>
<a href="#" onClick={this.addInput}>Add</a>
</div>
}
</div>
<DropDownField name="CountryOfResidence" value={CountryOfResidence} options={{
0: 'Please Select', 1: 'United Kingdom', 2: 'France', 3: 'United States', 4: 'Australia'
}} onChange={this.saveData.bind(this)} />
<div>
<div>
{selections.map(this.renderCountriesSelect)}
</div>
{this.props.selections.length < 4 &&
<div>
<a href="#" onClick={this.addInput}>Add</a>
</div>
}
</div>
)
}
}
const mapStateToProps = (state) => {
return {
store: state.AddDropdown,
selections: state.AddDropdown.selections,
}
}
const AddDropdown = connect(mapStateToProps, {saveSelect, removeSelect, saveSelectValue})(Component)
export default AddDropdown
action.js
export const ADD_DATA = 'ADD_DATA'
export const ADD_SELECT = 'ADD_SELECT'
export const REMOVE_SELECT = 'REMOVE_SELECT'
export const SAVE_SELECT_OPTION = 'SAVE_SELECT_OPTION'
export function addData(data) {
return { type: ADD_DATA, data }
}
export function saveSelect(data) {
return { type: ADD_SELECT, data }
}
export function removeSelect(data) {
return { type: REMOVE_SELECT, data }
}
export function saveSelectValue(data) {
return { type: SAVE_SELECT_OPTION, data }
}
reducer.js
import ObjectAssign from 'object.assign'
import { combineReducers } from 'redux'
import { ADD_DATA, ADD_SELECT, REMOVE_SELECT, SAVE_SELECT_OPTION } from './actions'
function AddDropdown(state = { selections: []}, action = {}){
switch (action.type){
case ADD_DATA:
return ObjectAssign({}, state, action.data)
case ADD_SELECT:
return {
...state,
selections: [].concat(state.selections, action.data),
}
case REMOVE_SELECT:
return {
...state,
selections: state.selections.filter((selection, index) => (index !== action.data)),
}
case SAVE_SELECT_OPTION:
return {
...state,
selections: state.selections.map((selection) => selection.id === action.data.id ? action.data : selection)
}
default:
return state
}
}
const FormApp = combineReducers({
AddDropdown
})
export default FormApp
I suggest isolating each set of dropdowns as a seperate component, then working on isolating each one's redux state. My library, redux-subspace was designed for this purpose.
index.jsx
import React from 'react'
import { connect } from 'react-redux'
import { SubspaceProvider } from 'redux-subspace'
import DropDownField from './form/drop-down-field'
import uuidV4 from 'uuid-v4'
import { saveSelect, removeSelect, saveSelectValue } from './actions.js'
class Component extends React.Component {
constructor(props) {
super(props);
}
saveData(e) {
let data = {}
data[e.target.name] = e.target.value
this.context.store.dispatch(
addData(data)
)
}
addInput = (e) => {
e.preventDefault()
this.props.saveSelect({id:uuidV4()})
}
removeInput = (index, e) => {
e.preventDefault()
this.props.removeSelect(index)
}
saveSelectValue = (e, id) => {
let data = {}
data.id = id
data.value = e.target.value
this.props.saveSelectValue(data)
}
renderSelections = (selection, index) => {
const selectedValue = selection.value || ''
const id = selection.id
return(
<div>
<DropDownField
key={id}
name={'field-'+ id}
value={selectedValue}
onChange = {(e) => { this.saveSelectValue(e, id) }}
required
options={this.props.options} />
<a href="#" onClick={ (e) => {this.removeInput(index, e) }}>Remove</a>
</div>
)
}
render(){
return (
<div>
<DropDownField name={this.props.name} value={this.props.store.value} options={this.props.options} onChange={this.saveData.bind(this)} />
<div>
{this.props.selections.map(this.renderSelections)}
</div>
{this.props.selections.length < 4 &&
<div>
<a href="#" onClick={this.addInput}>Add</a>
</div>
}
</div>
)
}
}
const mapStateToProps = (state) => {
return {
store: state,
selections: state.selections,
}
}
const SingleAddDropdown = connect(mapStateToProps, {saveSelect, removeSelect, saveSelectValue})(Component)
const AddDropdown = () => {
return (
<div>
<SubspaceProvider mapState={state => state.nationality} namespace="nationalities">
<SingleAddDropdown name="Nationality" options={{
0: 'Please Select',
1: 'British',
2: 'French',
3: 'American',
4: 'Australian'
}}/>
</SubspaceProvider>
<SubspaceProvider mapState={state => state.countryOfResidence} namespace="countryOfResidence">
<SingleAddDropdown name="Country of Residence" options={{
0: 'Please Select',
1: 'United Kingdom',
2: 'France',
3: 'United States',
4: 'Australia'
}}/>
</SubspaceProvider>
</div>
)
}
export default AddDropdown
reducer.js
import ObjectAssign from 'object.assign'
import { combineReducers } from 'redux'
import { namespaced } from 'redux-subspace'
import { ADD_DATA, ADD_SELECT, REMOVE_SELECT, SAVE_SELECT_OPTION } from './actions'
function AddDropdown(state = { selections: []}, action = {}){
switch (action.type){
case ADD_DATA:
return ObjectAssign({}, state, action.data)
case ADD_SELECT:
return {
...state,
selections: [].concat(state.selections, action.data),
}
case REMOVE_SELECT:
return {
...state,
selections: state.selections.filter((selection, index) => (index !== action.data)),
}
case SAVE_SELECT_OPTION:
return {
...state,
selections: state.selections.map((selection) => selection.id === action.data.id ? action.data : selection)
}
default:
return state
}
}
const FormApp = combineReducers({
namespaced(AddDropdown, "nationality"),
namespaced(AddDropdown, "countryOfResidence")
})
export default FormApp
NOTE: as per my comment there are some issues with this code and I have not attempted to clean them up for this example.

React Redux adding items to list carries over to other created lists.

I've been battling with this for too long, can someone point me to the right direction?
The issue: When i create a list I am able to update/delete it. I am also able to add items to it and update/delete these items. When I add another list, the items from the former gets carried over to the latter then I am unable to edit the items. If I delete the List and don't refresh the browser the items are still in the list. I need a way to tie the two together in a way that a list only knows about its items
Thanks in advance for the help.
/actions/lists.js
export const CREATE_LIST = 'CREATE_LIST'
export function createList(list) {
return {
type: CREATE_LIST,
id: uuid.v4(),
items: list.items || [],
...list
}
}
export const CONNECT_TO_LIST = 'CONNECT_TO_LIST'
export function connectToList(listId, itemId) {
return {
type: CONNECT_TO_LIST,
listId,
itemId
}
}
export const DISCONNECT_FROM_LIST = 'DISCONNECT_FROM_LIST'
export function disconnectFromList(listId, itemId) {
return {
type: DISCONNECT_FROM_LIST,
listId,
itemId
}
}
/actions/items.js
export const CREATE_ITEM = 'CREATE_ITEM'
export function createItem(item) {
return {
type: CREATE_ITEM,
item: {
id: uuid.v4(),
...item
}
}
}
export const UPDATE_ITEM = 'UPDATE_ITEM'
export function updateItem(updatedItem) {
return {
type: UPDATE_ITEM,
...updatedItem
}
}
/reducers/lists.js
import * as types from '../actions/lists'
const initialState = []
export default function lists(state = initialState, action) {
switch (action.type) {
case types.CREATE_LIST:
return [
...state,
{
id: action.id,
title: action.title,
items: action.items || []
}
]
case types.UPDATE_LIST:
return state.map((list) => {
if(list.id === action.id) {
return Object.assign({}, list, action)
}
return list
})
case types.CONNECT_TO_LIST:
const listId = action.listId
const itemId = action.itemId
return state.map((list) => {
const index = list.items.indexOf(itemId)
if(index >= 0) {
return Object.assign({}, list, {
items: list.items.length > 1 ? list.items.slice(0, index).concat(
list.items.slice(index + 1)): []
})
}
if(list.id === listId) {
return Object.assign({}, list, {
items: [...list.items, itemId]
})
}
return list
})
case types.DISCONNECT_FROM_LIST:
return state.map((list) => {
if(list.id === action.listId) {
return Object.assign({}, list, {
items: list.items.filter((id) => id !== action.itemId)
})
}
return list
})
default:
return state
}
}
/reducers/items.js
import * as types from '../actions/items'
const initialState = []
export default function items(state = initialState, action) {
switch (action.type) {
case types.CREATE_ITEM:
return [ ...state, action.item ]
case types.UPDATE_ITEM:
return state.map((item) => {
if(item.id === action.id) {
return Object.assign({}, item, action)
}
return item
})
case types.DELETE_ITEM:
return state.filter((item) => item.id !== action.id )
default:
return state
}
}
/components/List.jsx
import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import Items from './Items'
import Editor from './Editor'
import * as listActionCreators from '../actions/lists'
import * as itemActionCreators from '../actions/items'
export default class List extends React.Component {
render() {
const { list, updateList, ...props } = this.props
const listId = list.id
return (
<div {...props}>
<div className="list-header"
onClick={() => props.listActions.updateList({id: listId, isEditing: true})}
>
<div className="list-add-item">
<button onClick={this.addItem.bind(this, listId)}>+</button>
</div>
<Editor
className="list-title"
isEditing={list.isEditing}
value={list.title}
onEdit={title => props.listActions.updateList({id: listId, title, isEditing: false})}
/>
<div className="list-delete">
<button onClick={this.deleteList.bind(this, listId)}>x</button>
</div>
</div>
<Items
items={this.listItems}
onValueClick={id => props.itemActions.updateItem({id, isEditing: true})}
onEdit={(id, text) => props.itemActions.updateItem({id, text, isEditing: false})}
onDelete={itemId => this.deleteItem(listId, itemId)}
/>
</div>
)
}
listItems() {
props.list.items.map(id => state.items[
state.items.findIndex(item => item.id === id)
]).filter(item => item)
}
deleteList(listId, e) {
e.stopPropagation()
this.props.listActions.deleteList(listId)
}
addItem(listId, event) {
event.stopPropagation()
const item = this.props.itemActions.createItem({
text: 'New Shopping Item'
})
this.props.listActions.connectToList(listId, item.id)
}
deleteItem(listId, itemId) {
this.props.listActions.disconnectFromList(listId, itemId)
this.props.itemActions.deleteItem(itemId)
}
}
function mapStateToProps(state) {
return {
lists: state.lists,
items: state.items
}
}
function mapDispatchToProps(dispatch) {
return {
listActions: bindActionCreators(listActionCreators, dispatch),
itemActions: bindActionCreators(itemActionCreators, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(List)
/components/List.jsx
import React from 'react'
import List from './List.jsx'
export default ({lists}) => {
return (
<div className="lists">{lists.map((list) =>
<List className="list" key={list.id} list={list} id={list.id} />
)}</div>
)
}
/components/Items.jsx
import React from 'react'
import { connect } from 'react-redux'
import Editor from './Editor'
import Item from './Item'
export default class Items extends React.Component {
render () {
const {items, onEdit, onDelete, onValueClick, isEditing} = this.props
return (
<ul className="items">{items.map(item =>
<Item
className="item"
key={item.id}
id={item.id}
isEditing={item.isEditing}>
<Editor
isEditing={item.isEditing}
value={item.text}
onValueClick={onValueClick.bind(null, item.id)}
onEdit={onEdit.bind(null, item.id)}
onDelete={onDelete.bind(null, item.id)}
/>
</Item>
)}</ul>
)
}
}
export default connect(
state => ({
items: state.items
})
)(Items)
/components/Item.jsx
import React from 'react'
export default class Item extends React.Component {
render() {
const { id, isEditing, ...props } = this.props
return (
<li {...props}>{props.children}</li>
)
}
}
/components/App.jsx
class App extends React.Component {
handleClick = () => {
this.props.dispatch(createList({title: "New Shopping List"}))
}
render() {
const lists = this.props.lists
return (
<div>
<button
className="add-list"
onClick={this.handleClick}>Add Shopping List</button>
<Lists lists={lists}/>
</div>
)
}
}
export default connect(state => ({ lists: state.lists }))(App)
Assuming this is all meant to operate on a single array at a time, this part looks pretty suspicious:
case types.CREATE_LIST:
return [
...state,
{
id: action.id,
title: action.title,
items: action.items || []
}
]
That ...state is expanding whatever existing array there is into the new array that you're returning, and it doesn't sound like that's the behavior you actually want. My first guess is that when you create a new list, you probably want to just return the one new item inside, not the entire old list contents plus that new item.
Some of your other immutable-style update code also looks sort of complex. I know that "update a thing in the middle of an array" isn't always easy to deal with. You might want to take a look at this SO post on updating immutable data, which lists several ways to approach it. I also have a links repo that catalogs Redux-related libraries, and it has a list of immutable data management libraries that might make things easier for you.
Though #markerikson 's tip was part of the issue i was having, it didn't solve it completely. I had to fix my mapStateToProps to this
function mapStateToProps(state, props) {
return {
lists: state.lists,
listItems: props.list.items.map(id => state.items[
state.items.findIndex(item => item.id === id)
]).filter(item => item)
}
}
and remove the connect from previous implementation for the items alone in my Items.jsx

Categories

Resources