I have a problem with handleChange inputs in connection with Redux, it almost works, but not quite, because when I change state, in one field there is a change of state guess what I would like to achieve, but when I change in another field this state disappears from the first one and saves in the second one (on the picture below I showed what I mean). I have two such moderately convincing ideas, that is, in the reducer in the line "value: action.payload.num_building || '', and in the place of '' do ' ', so that there is always something there and then when I do the post I do the trim, so that there is no space, the post will fly as an empty value, or instead of '' do the state in the same place. num_building.value in one field and state.test.value in the other, but here the problem is that if I type something and then delete the whole input, there will always be the first letter of the text we typed and I don't know how to delete it :(
Slicer.js
import { createSlice } from '#reduxjs/toolkit';
export const numberBuildingSlice = createSlice({
name: 'number_building',
initialState: {
num_building: { key: 'num_building', value: '' },
test: { key: 'test', value: '' },
},
reducers: {
inputChange: (state, action) => {
return {
...state,
num_building: {
...state.num_building,
value: action.payload.num_building || state.num_building.value,
},
test: {
...state.test,
value: action.payload.test || ' ',
},
};
},
},
});
export const { inputChange } = numberBuildingSlice.actions;
export const numberBuildingState = (state) => state.number_building;
export default numberBuildingSlice.reducer;
Component
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import axios from 'axios';
import {
numberBuildingState,
inputChange,
} from '../redux/slices/numberBuildingSlice';
import LabelInput from '../reusable-components/LabelInput';
const URL = 'http://localhost:8000/numbers-buildings/';
function AddNumberBuilding() {
const numberBuilding = useSelector(numberBuildingState);
const dispatch = useDispatch();
const { value } = numberBuilding.num_building;
const handleChange = async (event) => {
const response = await dispatch(
inputChange({ [event.target.name]: event.target.value }),
);
console.log(response);
};
function postNumberBuilding() {
const jsonData = { num_building: value };
axios
.post(URL, jsonData)
.then((res) => {
console.log(res);
})
.catch((error) => console.error(error.response));
}
return (
<div>
<input
name="num_building"
onChange={handleChange}
{...numberBuilding.num_building}
/>
<input name="test" onChange={handleChange} {...numberBuilding.test} />
<input type="submit" onClick={postNumberBuilding} value="Add New!" />
</div>
);
}
export default AddNumberBuilding;
You are updating both the values in reducer, even though you are just sending one variable to be updated in dispatched function.
The solution is to use dynamic key accessor
reducers: {
inputChange: (state, action) => {
const key = Object.keys(action.payload)[0];
return {
...state,
[key ]: {
...state[action.payload[key]],
value: action.payload[key ] || state.num_building.value,
},
};
},
},
Related
I have a React app, the state is managed with Redux.
The user can search for a game and a multitude of results, whose titles loosely match the query, will appear on submitting. Every time the user enters another query, the previous results are replaced by the new ones.
After 5-6 searches, the app slows down considerably. After the 7th search, it stops working entirely, Chrome throwing a 'page not responding' notice.
The redux slice looks like this:
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
import rawg from '../../apis/rawg';
const initialState = {
results: [],
};
export const fetchGames = createAsyncThunk(
'gamesSearch/fetchGames',
async (query) => {
const response = await rawg.get('/games', {
params: {
search: query,
},
});
return response.data.results;
}
);
const gamesSearchSlice = createSlice({
name: 'gamesSearch',
initialState,
reducers: {},
extraReducers: {
[fetchGames.fulfilled]: (state, action) => {
const results = action.payload;
const parsedResults = results.map((result) => {
return {
name: result.name,
slug: result.slug,
backgroundImage: result.background_image,
genres: result.genres.map((genre) => genre.name).join(', '),
id: result.id,
released: result.released
? `${result.released.split('-')[2]}.${
result.released.split('-')[1]
}.${result.released.split('-')[0]}`
: null,
};
});
},
},
});
export default gamesSearchSlice.reducer;
export const selectResults = (state) => state.gamesSearch.results;
And the component from which the fetch is dispatched looks like so:
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { fetchGames } from './gamesSearchSlice';
const SearchBar = () => {
const [query, setQuery] = useState('');
const dispatch = useDispatch();
const onSubmit = (e) => {
e.preventDefault();
if (!query) return;
dispatch(fetchGames(query));
};
return (
<div className="searchbar">
<form onSubmit={onSubmit}>
<input
className="searchbar__input"
type="text"
placeholder="Search for a game..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
</form>
</div>
);
};
export default SearchBar;
Am I missing some detail about how React and Redux work together, or is it something wrong with my code from a fundamentals perspective (meaning: I am not handling data efficiently enough with JavaScript)?
I am trying to update an element from an array by adding an object as a property like shown in this picture
When a user clicks on a single node button, a modal appears the user fills the form and then it is addes as a property for this node.
But for some reason I get this type error that says that the updateElement is not a function.
BTW, I am using Redux & react-flow-renderer libraries.
Reducer
import * as types from '../actions/types';
const initialState = {
elements: []
};
const flow = (state = initialState, action) => {
switch (action.type) {
case types.UPDATE_ELEMENT:
return {
...state,
elements: state.elements.map((e) => {
if (e.id === action.payload.id) {
e = {
...e,
options: action.payload.options,
};
}
return e;
}),
};
default:
return state;
}
};
export default flow;
Action
import { UPDATE_ELEMENT } from './types';
export const updateElement = (data) => (dispatch) => {
dispatch({
type: UPDATE_ELEMENT,
payload: data,
});
};
Node modal
import React, { useState } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { updateElement } from '../../../../redux/actions/flow';
const VPCNodeModal = (props, { updateElement }) => {
const [formData, setFormData] = useState({
instance: '',
});
// options
const { instance } = formData;
const onFormChange = (e) =>
setFormData({ ...formData, [e.target.name]: e.target.value });
const onSubmitForm = () => {
const update = {
id: selectedElement.id,
options: formData,
};
updateElement(update);
};
return (
<>
<Modal {...props}>
<form
onSubmit={(e) => {
e.preventDefault();
onSubmitForm();
}}
>
<label>
<span> Instance name:</span>
<input
type='text'
name='instance'
value={instance}
onChange={onFormChange}
/>
</label>
<button type='submit'>Submit</button>
</form>
</Modal>
</>
);
};
VPCNodeModal.propTypes = {
updateElement: PropTypes.func.isRequired
};
export default connect(null, { updateElement })(VPCNodeModal);
Issue is while receiving the props.
change
const VPCNodeModal = (props, { updateElement }) => {
to
const VPCNodeModal = (props) => {
const { updateElement } = props;
updateElement is a props was passes in VPCNodeModal. So you should update like this with spread operator
const VPCNodeModal = ({ updateElement, ...props }) => {
I am trying to pass todos (initial state) and addNewTodo (methods) using React Context hook and typescript. I have tried multiple solutions but no success, still getting errors.
Partial generics doesn't give issue on context component, but it gives me the error Cannot invoke an object which is possibly 'undefined' while calling addNewTodo in todo form component.
Similarly, undefined and empty objects {} also give different errors. Can't figure out how to fix it. If I pass any then IntelliSense won't work.
Here is my code
TodoContext
import React, { useState, createContext, FC, useContext } from "react"
type Props = {
children: React.ReactNode,
}
interface TaskContextProps {
todos: Todo[],
addNewTodo: addNewTodo
}
const initialTodos: Array<Todo> = [
{ id: 1, text: 'buy some milk', completed: false },
{ id: 2, text: 'go to gym', completed: false }
]
export const TaskListContext = createContext<Partial<TaskContextProps>>({})
// export const TaskListContext = createContext<TaskContextProps>({})
// export const TaskListContext = createContext<TaskContextProps | undefined>(undefined)
const TaskListContextProvider: FC<Props> = ({ children }) => {
const [todos, setTodos] = useState(initialTodos)
const addNewTodo: addNewTodo = (newTodo) => {
setTodos([newTodo, ...todos])
}
return (
<TaskListContext.Provider value={{ todos, addNewTodo }}>
{children}
</TaskListContext.Provider>
)
}
Todo Form
import React, { useState, ChangeEvent, FormEvent, useContext } from 'react';
// import { useTaskList, TaskListContext } from '../context/TaskListContext';
import { TaskListContext } from '../context/TaskListContext';
const TodoForm = () => {
const [newTodo, setNewTodo] = useState('')
// const { addNewTodo } = useTaskList()
const { addNewTodo } = useContext(TaskListContext)
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setNewTodo(e.target.value)
}
const handleSubmit = (e: FormEvent<HTMLButtonElement>) => {
e.preventDefault()
const addTodo = { id: Math.random(), text: newTodo, completed: false }
if (newTodo.trim()) {
addNewTodo(addTodo)
}
else {
alert('Todo can\'t be empty')
}
setNewTodo('')
}
return (
<form>
<input placeholder='your todo...' value={newTodo} onChange={handleChange} />
<button onClick={handleSubmit}>Submit</button>
</form>
)
}
Your help will be appreciated.
To prevent TypeScript from telling that properties of an object are undefined we need to define them (using Partial sets every property as possibly undefined, which we want to avoid).
This means that we need to pass some value for each property when defining the context:
export const TaskListContext = createContext<TaskContextProps>({
todos: [],
addNewTodo: () => {}
});
This context initial values will be replaced as soon as the Provider is initialized, so they will never be read from a Component.
This way todos and addNewTodo will always have a value and TypeScript won't complain.
This is from a tutorial assignment from Dave Ceddia's Redux course, I am trying to display the initial state, which contains an array of objects, however it is simply returning undefined and not displaying anything. I am new to React, and I have hit a wall on getting 1) my buttons to display the state, and 2) default state to appear initially.
I have tried to have my component Buttons as a class, and constant.
I have tried stating my initialReducer in the default: return state; in my reducer as well. I have also tried different syntax for my dispatch actions, but nothing seems to be getting to the reducer.
index.js
import React, { Fragment } from "react";
import ReactDOM from "react-dom";
import { getAllItems, addEventToBeginning, addEventToEnd } from "./actions";
import { connect, Provider } from "react-redux";
import { store } from "./reducers";
const Buttons = ({
state,
getAllItems,
addEventToBeginning,
addEventToEnd
}) => (
<React.Fragment>
<ul>{state ? state.actions.map(item => <li>{item}</li>) : []}</ul>
<button onClick={getAllItems}> Display items </button>
<button onClick={addEventToBeginning}> addEventToBeginning </button>
<button onClick={addEventToEnd}> addEventToEnd </button>
</React.Fragment>
);
const mapDispatchToProps = { getAllItems, addEventToBeginning, addEventToEnd };
const mapStateToProps = state => ({
actions: state.actions,
sum: state.sum
});
connect(
mapStateToProps,
mapDispatchToProps
)(Buttons);
reducers.js
const initialState = {
actions: [
{ id: 0, type: "SALE", value: 3.99 },
{ id: 1, type: "REFUND", value: -1.99 },
{ id: 2, type: "SALE", value: 17.49 }
],
sum: 0
};
const newUnit = { id: Math.random * 10, type: "SALE", value: Math.random * 25 };
function eventReducer(state = initialState, action) {
switch (action.type) {
case ADD_EVENT_TO_BEGINNING:
const copy = { ...state };
copy.actions.unshift(newUnit);
return copy;
case ADD_EVENT_TO_END:
const copy2 = { ...state };
copy2.actions.unshift(newUnit);
return copy2;
cut out for cleanliness
case GET_ITEMS:
return {
...state,
actions: state.actions,
sum: state.sum
};
default:
return state;
}
}
export const store = createStore(eventReducer);
example of actions.js (they all follow same format)
export const ADD_EVENT_TO_BEGINNING = "ADD_EVENT_TO_BEGINNING";
export function addEventToBeginning() {
return dispatch => {
dispatch({
type: ADD_EVENT_TO_BEGINNING
});
};
}
UPDATE:
Thank you #ravibagul91 and #Yurui_Zhang, I cut everything but getAllItems out, and changed the state to:
const initialState = {
itemsById: [
{ id: 0, type: "SALE", value: 3.99 },
{ id: 1, type: "REFUND", value: -1.99 },
{ id: 2, type: "SALE", value: 17.49 }
]
};
class Form extends React.Component {
render() {
return (
<div>
{this.props.itemsById
? this.props.itemsById.map(item => (
<li>
{item.id} {item.type} {item.value}
</li>
))
: []}
<button onClick={this.getAllItems}> Display items </button>
</div>
);
}
}
const mapDispatchToProps = { getAllItems };
function mapStateToProps(state) {
return {
itemsById: state.itemsById
};
}
export function getAllItems() {
return dispatch => ({
type: "GET_ITEMS"
});
}
There are multiple problems with your code:
const mapStateToProps = state => ({
actions: state.actions,
sum: state.sum
});
Here you have mapped redux state fields to props actions and sum - your component won't receive a state prop, instead it will receive actions and sum directly.
so your component really should be:
const Button = ({
actions,
sum,
}) => (
<>
<ul>{actions && actions.map(item => <li>{item}</li>)}</ul>
</>
);
your mapDispatchToProps function is not defined correctly. It should be something like this:
// ideally you don't want the function names in your component to be the same as the ones you imported so I'm renaming it here:
import { getAllItems as getAllItemsAction } from "./actions";
// you need to actually `dispatch` the action
const mapDispatchToProps = (dispatch) => ({
getAllItems: () => dispatch(getAllItemsAction()),
});
Your reducer doesn't seem to be defined correctly as well, however you should try to fix the problems I mentioned above first :)
Try not to do too much in one go when you are learning react/redux. I'd recommend reviewing the basics (how the data flow works, how to map state from the redux store to your component, what is an action-creator, etc.).
As you are destructuring the props,
const Buttons = ({
state,
getAllItems,
addEventToBeginning,
addEventToEnd
}) => ( ...
You don't have access to state, instead you need to directly use actions and sum like,
const Buttons = ({
actions, // get the actions directly
sum, // get the sum directly
getAllItems,
addEventToBeginning,
addEventToEnd
}) => (
<React.Fragment>
//You cannot print object directly, need to print some values like item.type / item.value
<ul>{actions && actions.length && actions.map(item => <li>{item.type} {item.value}</li>)}</ul>
<button onClick={getAllItems}> Display items </button>
<button onClick={addEventToBeginning}> addEventToBeginning </button>
<button onClick={addEventToEnd}> addEventToEnd </button>
</React.Fragment>
);
Your mapDispatchToProps should be,
const mapDispatchToProps = dispatch => {
return {
// dispatching actions returned by action creators
getAllItems : () => dispatch(getAllItems()),
addEventToBeginning : () => dispatch(addEventToBeginning()),
addEventToEnd : () => dispatch(addEventToEnd())
}
}
Or you can make use of bindActionCreators,
import { bindActionCreators } from 'redux'
function mapDispatchToProps(dispatch) {
return {
dispatch,
...bindActionCreators({ getAllItems, addEventToBeginning, addEventToEnd }, dispatch)
}
}
In reducer, ADD_EVENT_TO_END should add element to end of the array, but you are adding again at the beginning using unshift. You should use push which will add element at the end of array,
case ADD_EVENT_TO_END:
const copy2 = { ...state };
copy2.actions.push(newUnit); //Add element at the end
return copy2;
Also your GET_ITEMS should be as simple as,
case GET_ITEMS:
return state;
(Only my 3rd post here, so please excuse any blatant issues).
The following is my Unit component, a child of a Course component (courses has_many units).
import React from 'react';
import { connect } from 'react-redux';
import { getUnits, addUnit, updateUnit } from '../reducers/units';
import { Container, Header, Form } from 'semantic-ui-react';
class Units extends React.Component {
initialState = { name: ''}
state = { ...this.initialState }
componentDidUpdate(prevProps) {
const { dispatch, course } = this.props
if (prevProps.course.id !== course.id)
dispatch(getUnits(course.id))
}
handleSubmit = (e) => {
debugger
e.preventDefault()
debugger
const unit = this.state
const { dispatch } = this.props
if (unit.id) {
debugger
dispatch(updateUnit(unit))
} else {
debugger
dispatch(addUnit(unit))
this.setState({ ...this.initialState })
}
}
handleChange = (e) => {
const { name, value } = e.target
this.setState({ [name]: value })
}
units = () => {
return this.props.units.map( (unit, i) =>
<ul key={i}>
<li key={unit.id}> {unit.name}</li>
<button>Edit Module Name</button>
<button>Delete Module</button>
</ul>
)
}
render() {
const { name } = this.state
return (
<Container>
<Header as="h3" textAlign="center">Modules</Header>
{ this.units() }
<button>Add a Module</button>
<Form onSubmit={this.handleSubmit}>
<Form.Input
name="name"
placeholder="name"
value={name}
onChange={this.handleChange}
label="name"
required
/>
</Form>
</Container>
)
}
}
const mapStateToProps = (state) => {
return { units: state.units, course: state.course }
}
export default connect(mapStateToProps)(Units);
The following is its reducer:
import axios from 'axios';
import { setFlash } from './flash'
import { setHeaders } from './headers'
import { setCourse } from './course'
const GET_UNITS = 'GET_UNITS';
const ADD_UNIT = 'ADD_UNIT';
const UPDATE_UNIT = 'UPDATE_UNIT';
export const getUnits = (course) => {
return(dispatch) => {
axios.get(`/api/courses/${course}/units`)
.then( res => {
dispatch({ type: GET_UNITS, units: res.data, headers: res.headers })
})
}
}
export const addUnit = (course) => {
return (dispatch) => {
debugger
axios.post(`/api/courses/${course}/units`)
.then ( res => {
dispatch({ type: ADD_UNIT, unit: res.data })
const { headers } = res
dispatch(setHeaders(headers))
dispatch(setFlash('Unit added successfully!', 'green'))
})
.catch( (err) => dispatch(setFlash('Failed to add unit.', 'red')) )
}
}
export const updateUnit = (course) => {
return (dispatch, getState) => {
const courseState = getState().course
axios.put(`/api/courses/${course.id}/units`, { course })
.then( ({ data, headers }) => {
dispatch({ type: UPDATE_UNIT, course: data, headers })
dispatch(setCourse({...courseState, ...data}))
dispatch(setFlash('Unit has been updated', 'green'))
})
.catch( e => {
dispatch(setHeaders(e.headers))
dispatch(setFlash(e.errors, 'red'))
})
}
}
export default (state = [], action) => {
switch (action.type) {
case GET_UNITS:
return action.units;
case ADD_UNIT:
return [action.unit, ...state]
case UPDATE_UNIT:
return state.map( c => {
if ( c.id === action.unit.id )
return action.unit
return c
})
default:
return state;
}
};
Note: My reducer is working for my getUnits and rendering the units properly.
Note also: when I try to submit a new unit, it ignores all of the debuggers in my handleSubmit and the debuggers in my addUnits (in the reducer), but somehow renders the flash message of "Failed to add units".
Then the console logs the error seen in the title of this post.
I raked my routes and my post is definitely supposed to go to the route as it is.
I have tried passing in the unit and the course in various ways without any change to the error.
How can it hit the flash message without hitting any of the debuggers?
How do I fix this [object%20Object]issue?
Thanks in advance!
The variable course in the following line
axios.get(`/api/courses/${course}/units`)
is an object. When you try to convert an object to a string in JavaScript, [object Object] is the result. The space is then converted to %20 for the URL request.
I would look at the contents of the course variable. Likely, what you actually want in the URL is something inside of course. Perhaps course.id.
If you are still having issues, you'll need to explain what value should go in the URL between /courses/ and /units, and where that data exists.
You are invoking addUnit and updateUnit with a parameter that is equal to this.state in handleSubmit
const unit = this.state
addUnit(unit)
As this.state is of type object, it is string concatenated as object%20object.
getUnit works fine as the parameter passed there comes from the prop course. Check the value of state inside handleSubmit.