add inputs dynamically react redux - reactjs

hello I will ask this question again because I still can’t find a answer
I try to create a form similar to google form with react and redux
each question is represented by: QuestionCard
contains a title and can have several types (short question, long question, choice ,multiple, single choice ...)
I manage to add cardQuestion and delete them
my problem is that when I select single choice I want to give the user the possibility to add the number of choices he wants but it does not work
this is my reducer
const initialState = {
formId: '',
form: {
title: '',
description: ''
},
basicForm: {
fullName: '',
age: '',
saved: false
},
cardQuestion: [{
title: '',
choix: [],
type: '',
}],
error: "",
loading: false,
}
const addRequestReducer = (state = initialState, action) => {
switch (action.type) {
case 'SET_STATE':
return { ...state, ...action.payload }
default:
return state
}
}
i use a global state
and this how i try to add choices
1- i handle change of inputs (each choice) like this :
const { cardQuestion } = useSelector((state) => state.addRequest)
const choice = cardQuestion.map(e => e.choix)
const onChangeInput = (i, key, value) => {
console.log(value)
dispatch({
type: 'SET_STATE',
payload: {
cardQuestion: cardQuestion.map((elem, index) => {
console.log(i)
console.log(i === index)
if (i !== index) return elem
return { ...elem, [`${key}`]: value }
})
}
})
}
2- use the ChangeInptut like this
<div >{choice.map((elem, i) => <div key={i}>
<Input value={elem.choix} onChange={(e) => onChangeInput(i, 'choix', e.target.value)} />
</div>
3- button to add new input (choice)
<Button onClick={() => dispatch({
type: 'SET_STATE',
payload: {
cardQuestion: [...cardQuestion, { choix: '' }]
}
})} >Add</Button>
</div>
but it give me results like this :
and when i add a choice in question card1 it is also added in questioncard2
how can i resolve this , any help will be appreciated

This
cardQuestion: [...cardQuestion, { choix: '' }]
adds a new card without a title or type to the list of cards. If I understand you correctly, you want to add a new choice to an existing card, which will look something like
const newCards = [...cardQuestion]
newCards[currentCardPosition] = {...newCards[currentCardPosition]}
newCards[currentCardPosition].choix = [...newCards[currentCardPosition].choix, '' ]
but I don't see where currentCardPosition will come from

Related

How to get values and add them to dynamic form fields?

I have created a form that can add fields dynamically(add more). I need to auto populate data that is taken through an id of a select field and add them into some input fields. problem is I am getting the expected result but not getting the value added to those specific data fields. It auto populated every dynamic field that is created relevant to its attribute name.
Here's my states
const [productSelectedList, setproductSelectedList] = useState([])
const [inputList, setInputList] = useState({
user_id: '',
agent_id: '',
tonnes_complete: '',
dockets_complete: '',
customer_id: '',
customer_address: '',
spreading_unit: '',
payment_method: '',
spread_completed_data: '',
spread_rate: '',
payment_status: '',
order_status: '',
order_list: [
{
docket_number: '',
operator_id: '',
product_id: '',
product_mix_id: '',
product_docket: '',
quantity: '',
quantity_rate: '',
spread_status: '',
driver_comments: '',
},
],
})
Here's my onchange and how i am selecting the data from an api
const handleChangeProductMix = (e, index) => {
const { name, value } = e.target
const list = { ...inputList } //<-- object, not array
list.order_list[index][name] = value
const product_mix_id = list.order_list[index][name]
axios.get(`api/edit_product_mix/${product_mix_id}`).then((res) => {
if (res.data.status === 200) {
setproductSelectedList(res.data.product_mix)
} else if (res.data.status === 404) {
swal('Error', res.data.message, 'error')
history.push('/products/productmix')
}
})
setInputList(list)
console.log(productSelectedList)
}
const handleChange = (e, index) => {
const { name, value } = e.target
const list = { ...inputList } //<-- object, not array
console.log(list)
list.order_list[index][name] = value
setInputList(list)
}
and here's my input field
<div className="col-lg-4 mb-2">
<div className="form-group">
<label className="pb-2">Product Mix Name</label>
<input className="form-control" type="text" name="product_docket" onChange={(e)=> handleChange(e, i)}
value={productSelectedList.product_docket}
placeholder="Select Customer Code"
/>
</div>
</div>
When the auto populated data is added and when i click add more the data is being duplicated and replaced with recently selected option.
here's how i add fields dynamically
const handleAddInput = (e) => {
e.preventDefault()
setInputList({
...inputList,
order_list: [
...inputList.order_list,
{
docket_number: '',
operator_id: '',
product_id: '',
product_mix_id: '',
product_docket: '',
quantity: '',
quantity_rate: '',
spread_status: '',
driver_comments: '',
},
],
})
}
You are directly modifying React state with these lines in handleChange and handleChangeProductMix:
const list = { ...inputList } //<-- object, not array
list.order_list[index][name] = value
You need to deep clone the inputList state instead, so that you are not directly modifying one of the values on a property of one of the objects in the array on inputList.order_list.
Here are a few different ways to do it:
JSON.parse:
const list = JSON.parse(JSON.stringify(inputList));
Spread syntax (like in handleAddInput):
const list = {
...inputList,
order_list: inputList.order_list.map(o => ({...o})),
};
A simple clone function:
/**
* This is a naive clone implementation, only meant to be used with
* plain objects/arrays and scalar values.
*/
function clone (value) {
if (typeof value !== 'object') return value;
if (value === null) return value;
if (Array.isArray(value)) return value.map(v => clone(v));
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, clone(v)]));
}
const list = clone(inputList);

Delete one item in an array ReactJS (React Hooks and Redux)

I'm creating a recipe application using MERN stack.
The issue I am stuck on is trying to delete an ingredient found in an array, inside a recipe object. My recipe object looks like this:
MongoDB Recipe Object
Each ingredient has a cross next to it that allows you to click on it to remove.
<div>
<ol className='pad5px20px'>
{addIngredients.ingredients.map(data => (
<div className='ingredient-item padTB5px' key={data}>
<li>{data}</li>
<span onClick={() => removeIngredient(data)}>
<i className='fas fa-trash'></i>
</span>{' '}
      
</div>
))}
</ol>
</div>
The addIngredients and removeIngredient functions look like this:
const addIngredient = e => {
e.preventDefault();
if (query === '') return;
addIngredients.ingredients.push(query);
setIngredients(addIngredients);
setRecipe(prevState => ({
...prevState,
ingredients: [
...prevState.ingredients,
{ id: Date.now(), ingredient: query }
]
}));
};
const removeIngredient = data => {
const results = addIngredients.ingredients.filter(
e => e.ingredients !== data
);
setIngredients(
addIngredients.ingredients.filter(e => e.ingredients !== data)
);
};
Every time I remove an ingredient from my list I get an error that states "TypeError: Cannot read property 'map' of undefined".
Is there something that i'm missing here? I have been working on this app for the past couple of months now and I am stuck on this particular bit. I thought a better way would be to use Redux as I have been able to delete a whole recipe using a reducer:
case DELETE_RECIPE:
return {
...state,
recipes: state.recipes.filter(recipe => recipe._id !== action.payload),
loading: false
};
but how would I be able to target one particular ingredient?
Any suggestions would be greatly appreciated.
I added notes to your code problems)
const addIngredient = e => {
e.preventDefault();
if (query === '') return;
***STATE MUTATION***
addIngredients.ingredients.push(query);
setIngredients(addIngredients);
setRecipe(prevState => ({
...prevState,
ingredients: [
...prevState.ingredients,
{ id: Date.now(), ingredient: query }
]
}));
};
const removeIngredient = data => {
const results = addIngredients.ingredients.filter(
e => e.ingredients !== data
);
***You're adding ingredients object instead of addIngredients as you used in addIngredient method***
setIngredients(
addIngredients.ingredients.filter(e => e.ingredients !== data)
);
};
addIngredients.ingredients.filter(e => e.ingredients !== data) returns filtered ingredients instead of addIngredients with filtered ingredients field
How it should be
const addIngredient = e => {
e.preventDefault();
if (query === '') return;
setIngredients({
...addIngredients,
ingredients: [
...addIngredients,
query
]
});
setRecipe(prevState => ({
...prevState,
ingredients: [
...prevState.ingredients,
{ id: Date.now(), ingredient: query }
]
}));
};
const removeIngredient = data => {
const results = addIngredients.ingredients.filter(
e => e.ingredients !== data
);
setIngredients(
{
...addIngredients,
ingredients: results
});
};

React Hooks & redux cause unnecesary rendering and some unexpected effects

Expected behavior - I load an photo object and render it in my component.
With the code below I do achieve the result, but with some unexpected effects which in the end make this component not useful - see further description in two parts.
I really want to dive in the cause of all of it and to understand how can I prevent such behavior in my app.
I did try some suggestions from others peoples similar problems, but nothing did helped. I won't list here the things I've tried, because, obviously, everytime I tried - I did something wrong since it didn't help me.
I will be grateful for any ideas and suggestions - I miss something and can't understand what is it.
Part 1 of the problem.
While loading this component for the first time and/or refreshing it - I get multiple rerenders. From the Redux DevTools I can observ that the actions fire for two times, console-logging any received from the photo value shows that this value appears in the console 6 times (first 3 times - with initial state from the redux-store, after - with the expected fetched from the photo object value).
Part 2 of the problem.
When I open the next photo (the same component, just passing different match.params.id) - the component starting to rerender apparently for random times. It might take some seconds to complete this rerender loop, so it rerenders sometimes for dozens, sometimes for more then a 100 time, but always in the end is rendering the needed info.
Analyzing the logs I saw that the the values of fetched now photo are just switching in the loop with the values of the photo fetched before. The looping stops with the correct values. And where from the previos values are coming - I can't figure out, because before fetching a new photo object I clear all the data of the previous in the redux state.
Component:
//IMPORTS
const Photo = ({ getPhotoById, photo, loading, match }) => {
const [photoData, setPhotoData] = useState({
photoID: match.params.id,
imgUrl: '',
photoFileName: '',
title: '',
description: '',
albumID: '',
albumName: '',
categoryID: '',
categoryName: '',
categoryID2: '',
categoryName2: '',
categoryID3: '',
categoryName3: '',
locationID: '',
locationName: '',
contributorID: '',
contributorName: '',
contributorWeb: '',
source: '',
sourceWeb: '',
author: '',
periodID: '',
periodName: '',
license: ''
});
const {
photoID,
imgUrl,
photoFileName,
title,
description,
albumID,
albumName,
categoryID,
categoryName,
categoryID2,
categoryName2,
categoryID3,
categoryName3,
locationID,
locationName,
contributorID,
contributorName,
source,
sourceWeb,
author,
periodID,
periodName,
license
} = photoData;
useEffect(() => {
getPhotoById(photoID);
}, [getPhotoById, photoID]);
useEffect(() => {
if (loading === false) {
const {
photoID,
imgUrl,
photoFileName,
title,
description,
albumID,
albumName,
categoryID,
categoryName,
categoryID2,
categoryName2,
categoryID3,
categoryName3,
locationID,
locationName,
contributorID,
contributorName,
source,
sourceWeb,
author,
periodID,
periodName,
license
} = photo;
setPhotoData({
photoID,
imgUrl,
photoFileName,
title,
description,
albumID,
albumName,
categoryID,
categoryName,
categoryID2,
categoryName2,
categoryID3,
categoryName3,
locationID,
locationName,
contributorID,
contributorName,
source,
sourceWeb,
author,
periodID,
periodName,
license
});
}
}, [loading]);
useEffect(() => {
if (!loading) {
initOpenseadragon();
}
}, [loading]);
console.log(photoFileName, 'photoFileName');
const initOpenseadragon = () => {
OpenSeadragon({
id: 'viewer',
tileSources: `/uploads/tiles/${photoFileName}.dzi`,
prefixUrl: '/images/osd/',
showZoomControl: true,
showHomeControl: true,
showFullPageControl: true,
showRotationControl: true
});
};
return !photo && !loading ? (
<NotFound />
) : (
<Fragment>
SOME JSX
</Fragment>
);
};
Photo.propTypes = {
getPhotoById: PropTypes.func.isRequired,
// photo: PropTypes.object.isRequired,
loading: PropTypes.bool.isRequired
};
const mapStateToProps = state => {
return {
photo: state.photo.photo,
loading: state.photo.loading
};
};
export default connect(
mapStateToProps,
{ getPhotoById }
)(Photo);
ACTION:
export const getPhotoById = photo_id => async dispatch => {
try {
dispatch({ type: CLEAR_PHOTO });
dispatch({ type: LOAD_PHOTO });
const res = await axios.get(`/api/photo/${photo_id}`);
dispatch({
type: GET_PHOTO,
payload: res.data
});
} catch (err) {
dispatch({
type: PHOTOS_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
REDUCER
const initialState = {
photo: null,
photos: [],
loading: true,
error: {}
};
const photo = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case GET_PHOTO:
return {
...state,
photo: payload,
loading: false
};
case LOAD_PHOTO:
return {
...state,
loading: true
};
case CLEAR_PHOTO:
return {
...state,
photo: null,
loading: false
};
case PHOTOS_ERROR:
return {
...state,
error: payload,
loading: false
};
default:
return state;
}
};
export default photo;
Your problem is that you are adding getPhotoById as a dependency of your hook see this article about the dependency array.
If you want to prevent the re-render you can do the following:
const ref = useRef();
getPhotoByIdRef.current = getPhotoById
useEffect(() => {
getPhotoByIdRef(match.params.id)
}, [getPhotoByIdRef, match.params.id]);

React filter payload arrays

I need help filtering an array of different values.Im using React. Thank you in advance!!!
I have a payload that contains 3 different types of arrays, each type of array has the following key values.
myPayload : array(3)
0: {name: “John “ , age: “24” , height: “174cm” , **type:
“student”**}.
1: {name: “Jane” , age: “49” , height: “168cm” ,**type: “teacher”**}
2: { name: “schoolName” , country: “UnitedStated” , state: “IL”,
**type:”building”**}
Cards of type "student" & "teacher" contain the same fields but the "building" card has different fields from the others.
I am rendering the arrays as cards and my goal is to be able to search through all values of the arrays/cards.
This is what I have in my cards.reducer
import { FILTER_CARDS, FETCH_CARDS } from '../X/Y';
const initialState = { myPayload: [], filteredPayload: [] };
export default function (state = initialState, action) {
const { type, myPayload = [], value } = action;
const handler = {
[FETCH_CARDS]: {
...state,
myPayload,
},
[FILTER_CARDS]: {
...state,
filteredPayload: state.myPayload.filter(({ name }) => name.toLowerCase().match(value)),
},
};
return handler[type] || state;
}
The above only filters through the "name" field of each card.
I tried filtering through all values (name, age, height, type, schoolName, country, state) by making changes to "FILTER_CARDS" but that breaks and gives me an error of
"cannot read property 'toLowerCase' of undefined"
[FILTER_CARDS]: {
...state,
filteredPayload: (state.myPayload.type == ('student' || 'teacher')
? (state.myPayload.filter(({ name }) => name.toLowerCase().match(value)),
state.myPayload.filter(({ age }) => age.toLowerCase().match(value)),
state.myPayload.filter(({ height }) => height.toLowerCase().match(value)))
: (state.myPayload.filter(({ name }) => name.toLowerCase().match(value)),
state.myPayload.filter(({ country }) => country.toLowerCase().match(value)),
state.assetsPayload.filter(({ state }) => state.toLowerCase().match(value)))),
},
any input on how to filter through all values in this scenario?
You could just search in the values of the object, so you do not really need to care about the key names.
filteredPayload: state.myPayload.filter(item => (
Object.values(item).some(objValue => (
objValue.toLowerCase().match(value)
))
))
I solved this by doing the following
filteredPayload: state. myPayload.filter(item => {
switch (item.type) {
case 'building':
return (
item.name.toLowerCase().match(value)
|| item.country.toLowerCase().match(value)
|| item.state.toLowerCase().match(value)
);
case 'student':
case 'teacher':
return (
item.name.toLowerCase().match(value)
|| item.age.toLowerCase().match(value)
|| item.height.toLowerCase().match(value)
);
}
return item;
}),
Thank you all for helping out ^_^

Filter products depend on another ACTION in React-native Redux

I have an app which get all categories and products from the server with Redux ACTIONS. I need to filter products with a category Id. after load data action is complete, i call another action to filter products but i'm a little bit confused.
There is codes of few parts of the app:
ProductsActions:
export const GET_INITIAL_PRODUCTS_DATA = "GET_INITIAL_PRODUCTS_DATA";
export const GET_INITIAL_PRODUCTS_DATA_RESULT = "GET_INITIAL_PRODUCTS_DATA_RESULT";
export const GET_INITIAL_PRODUCTS_DATA_ERROR = "GET_INITIAL_PRODUCTS_DATA_ERROR";
export const FILTER_PRODUCTS_BY_CATEGORY_ID = "FILTER_PRODUCTS_BY_CATEGORY_ID";
export const getInitialProductsData = () => ({
type: GET_INITIAL_PRODUCTS_DATA
});
export const filterProductsByCategoryId = categoryId => ({
type: FILTER_PRODUCTS_BY_CATEGORY_ID,
categoryId
});
ProductsReducers:
import {
GET_INITIAL_PRODUCTS_DATA,
GET_INITIAL_PRODUCTS_DATA_RESULT,
GET_INITIAL_PRODUCTS_DATA_ERROR,
FILTER_PRODUCTS_BY_CATEGORY_ID
} from "../actions/products";
const initialState = {
isFetching: false,
data: {},
error: null
};
const filterProductsByCategoryId = (state, action) => {
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case GET_INITIAL_PRODUCTS_DATA:
return {
...state,
isFetching: true
};
case GET_INITIAL_PRODUCTS_DATA_RESULT:
return {
...state,
isFetching: false,
data: action.result
};
case GET_INITIAL_PRODUCTS_DATA_ERROR:
return {
...state,
isFetching: false,
error: action.error
};
case FILTER_PRODUCTS_BY_CATEGORY_ID:
return {
...state,
data: filterProductsByCategoryId(state, action.categoryId)
};
default:
return state;
}
};
export default reducer;
And there is my code to call filter action:
filterProducts = (title = "A") => {
const _categories = Object.values(this.props.categories);
const selectedCategory = _categories.find(
category => category.title === title
);
this.props.dispatch(filterProductsByCategoryId(selectedCategory.id));
My questions is:
A) Is there is a way to filter my data and display them in UI and refresh them without using ACTIONS way??
B) If A's answer is No!, How can i get my state.data and filter them in FILTER_PRODUCTS_BY_CATEGORY_ID?
Thanks.
You can use the Array.prototype.filter() to return filtered result.
keep in mind that this will return an array and not a single value, which is a good thing if you are using this filter within your reducer. because your reducer's shape is an array and not an object.
Running example:
const myData = [{
name: 'some name',
id: 1
}, {
name: 'some name2',
id: 2
}, {
name: 'some name3',
id: 3
}, {
name: 'some name4',
id: 4
}]
const filterProductsByCategoryId = (state, action) => {
return state.filter(c => c.id === action.categoryId);
};
const result = filterProductsByCategoryId(myData, {categoryId: 2});
console.log(result);
I think it is more appropriate to create a selector for a singular product that will handle this kind of action, this way you will be able to return an object instead of an array with one product in it.
Not to mention the benefits of using reselect to do some memoizations.
For this task you can use the Array.prototype.find():
const myData = [{
name: 'some name',
id: 1
}, {
name: 'some name2',
id: 2
}, {
name: 'some name3',
id: 3
}, {
name: 'some name4',
id: 4
}]
const filterProductsByCategoryId = (state, id) => {
return state.find(c => c.id === id);
};
const result = filterProductsByCategoryId(myData, 2);
console.log(result);

Resources