How can I prevent states from resetting after a http request? - javascript

when I send the server an HTTP request ( patch request in the checkbox onChange function) and update the state other states will be deleted until I reload the page and they will be back
so how can I update the states without losing the others?
I'm not totally sure but I think the problem is where I'm updating the state with the response I get from the server I think I'm not updating the state and I'm just adding the new response to it and replacing the others
here's my code
const Form = () => {
const [todos, setTodos] = React.useState([]);
const debounce = (func, timeout = 350) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, timeout);
};
};
const saveInput = (e, id) => {
const x = !e.target.checked;
console.log(x);
axios
.patch(`http://127.0.0.1:8000/todo/todos/${id}/`, {
completed: x,
})
.then(
(response) => {
console.log(response.data);
setTodos([response.data]);
},
(error) => {
console.log(error);
}
);
};
const processChange = debounce((e, id) => saveInput(e, id));
useEffect(() => {
axios.get("http://127.0.0.1:8000/todo/todos/").then((response) => {
setTodos(response.data);
});
}, []);
return (
<form>
<h1>Todo list</h1>
<button>Add</button>
<div>
{todos.map((todo) => (
<ul key={todo.id}>
<li>{todo.title}</li>
<li>{todo.description}</li>
<button onClick={() => deleteHandler(todo.id)}>delete</button>
<input
type="checkbox"
placeholder="completed"
onChange={(e) => processChange(e, todo.id)}
checked={todo.completed}
/>
</ul>
))}
</div>
</form>
);
};
export default Form;

Inside your saveInput function you are resetting your todos with the new response data. Thus you are losing the data from your component initialization. What you need to do is to destructure your existing data and add them with your new payload.
You can either do this:
setTodos([...todos, response.data]);
Or this:
setTodos((prevState) => ([...prevState, response.data]))
The second option is the best practice as this returns your state correctly.
Hope this helps.

Try This.
const saveInput = (e, id) => {
const x = !e.target.checked;
console.log(x);
const newTodos = [...todos];
const filteredTodos = newTodos.filter(todo => todo.id !== id);
axios
.patch(`http://127.0.0.1:8000/todo/todos/${id}/`, {
completed: x,
})
.then(
(response) => {
console.log(response.data);
filteredTodos.push(response.data);
setTodos(filteredTodos);
},
(error) => {
console.log(error);
}
);
};

So Morteza's answer worked but it had a tiny problem which was when i updated the state the order of the state ( array ) would change because of .push() method which pushes the item to the last index
here is the solution which works fine and won't change the orders:
setTodos(val => val.map(item => item.id === response.data.id ? (response.data) : item));
It's getting the todos current state, mapping and checking if each of its item.id is equal to the item.id from the server and if it is, then replaced and updated with a response.data, if not then nothing changes.

Related

Redux store is updated but view is not

I have the parent Posts.js component which map every object in posts array. In this function I try to filter all notes have same post_id as id of the current mapped post object. All stored in filteredNotes variable. Then I pass it to each child. Now the issue. When I want to add new note in specific post, the view doesn't update (new note was not added to the list) although the database and redux store has been updated successfully.
But when I try to remove that filter function, everything works just fine so I guess the main problem is there. Any idea how to fix this? Thanks
Posts.js
const posts = useSelector((state) => state.post.posts);
const notes = useSelector((state) => state.notes.notes);
useEffect(() => {
dispatch(getPosts());
dispatch(getNotes());
}, []);
const addNoteHandle = (val) => {
dispatch(addNote({new_note: val}));
}
return (
<div className="post__page">
<div className="post__list">
{posts.map((data) => {
let filteredNotes = notes.filter((i) => i.post_id === data.id);
return <Post data={data} notes={filteredNotes} />;
})}
</div>
<PostForm addNewNote={addNoteHandle} />
</div>
);
Post.js
export const Post = ({ data, notes }) => {
return (
<div className="post__item">
<div className="post__title">{data.title}</div>
<div className="post__note">
{notes.map(note => <div>{note.text}</div>)}
</div>
</div>
);
};
NoteForm.js
const NoteForm = ({ addNewNote }) => {
const [text, setText] = useState("");
return (
<div>
<Input value={text} onChange={(e) => setText(e.target.value)} />
<Button type="primary" onClick={() => addNewNote(text)} >
<SendOutlined />
</Button>
</div>
);
};
Action
export const addNote = ({ new_note }) => async (dispatch) => {
try {
const res = await axios.post("http://localhost:9000/api/note", new_note);
dispatch({ type: ADD_NOTE, payload: res.data });
} catch (err) {
dispatch({ type: NOTE_FAIL });
}
};
Reducer
case ADD_NOTE:
return {
...state,
notes: [...state.notes, payload]
};
use useSelector to get the component value from redux store. for some reason hook setText will not work to update the page component. I had a similar problem and could not find any solution. This code may help:
let text ='';
text = useSelector((state) =>
state.yourReducer.text);
Now show your text wherever you want
this will fix the issue until you find real solution

How to get react component with useReducer to rerender after axios call?

I am trying to learn state management with the useReducer hook so I have built a simple app that calls the pokeAPI. The app should display a random pokemon, and add more pokemons to the screen as the 'capture another' button is pressed.
However, it rerenders the component with the initialized and empty Card object before populating the Card from the axios call. I've tried at least 3 different solutions based on posts from stackoverflow.
In each attempt I have gotten the same result: the app displays an undefined card on, even though the state is updated and not undefined, it just was updated slightly after the rerendering. When clicked again that prior undefined gets properly rendered but there is now a new card displayed as undefined.
I am still getting the hang of react hooks (no pun intended!), async programming, and JS in general.
Here is the app:
https://stackblitz.com/edit/react-ts-mswxjv?file=index.tsx
Here is the code from my first try:
//index.tsx
const getRandomPokemon = (): Card => {
var randomInt: string;
randomInt = String(Math.floor(898 * Math.random()));
let newCard: Card = {};
PokemonDataService.getCard(randomInt)
.then((response) => {
//omitted for brevity
})
.catch((error) => {
//omitted
});
PokemonDataService.getSpecies(randomInt)
.then((response) => {
//omitted
})
.catch((error) => {
//omitted
});
return newCard;
};
const App = (props: AppProps) => {
const [deck, dispatch] = useReducer(cardReducer, initialState);
function addCard() {
let newCard: Card = getRandomPokemon();
dispatch({
type: ActionKind.Add,
payload: newCard,
});
}
return (
<div>
<Deck deck={deck} />
<CatchButton onClick={addCard}>Catch Another</CatchButton>
</div>
);
};
//cardReducer.tsx
export function cardReducer(state: Card[], action: Action): Card[] {
switch (action.type) {
case ActionKind.Add: {
let clonedState: Card[] = state.map((item) => {
return { ...item };
});
clonedState = [...clonedState, action.payload];
return clonedState;
}
default: {
let clonedState: Card[] = state.map((item) => {
return { ...item };
});
return clonedState;
}
}
}
//Deck.tsx
//PokeDeck and PokeCard are styled-components for a ul and li
export const Deck = ({ deck }: DeckProps) => {
useEffect(() => {
console.log(`useEffect called in Deck`);
}, deck);
return (
<PokeDeck>
{deck.map((card) => (
<PokeCard>
<img src={card.image} alt={`image of ${card.name}`} />
<h2>{card.name}</h2>
</PokeCard>
))}
</PokeDeck>
);
};
I also experimented with making the function that calls Axios a promise so I could chain the dispatch call with a .then.
//index.tsx
function pokemonPromise(): Promise<Card> {
var randomInt: string;
randomInt = String(Math.floor(898 * Math.random()));
let newCard: Card = {};
PokemonDataService.getCard(randomInt)
.then((response) => {
// omitted
})
.catch((error) => {
return new Promise((reject) => {
reject(new Error('pokeAPI call died'));
});
});
PokemonDataService.getSpecies(randomInt)
.then((response) => {
// omitted
})
.catch((error) => {
return new Promise((reject) => {
reject(new Error('pokeAPI call died'));
});
});
return new Promise((resolve) => {
resolve(newCard);
});
}
const App = (props: AppProps) => {
const [deck, dispatch] = useReducer(cardReducer, initialState);
function asyncAdd() {
let newCard: Card;
pokemonPromise()
.then((response) => {
newCard = response;
console.log(newCard);
})
.then(() => {
dispatch({
type: ActionKind.Add,
payload: newCard,
});
})
.catch((err) => {
console.log(`asyncAdd failed with the error \n ${err}`);
});
}
return (
<div>
<Deck deck={deck} />
<CatchButton onClick={asyncAdd}>Catch Another</CatchButton>
</div>
);
};
I also tried to have it call it with a side effect using useEffect hook
//App.tsx
const App = (props: AppProps) => {
const [deck, dispatch] = useReducer(cardReducer, initialState);
const [catchCount, setCatchCount] = useState(0);
useEffect(() => {
let newCard: Card;
pokemonPromise()
.then((response) => {
newCard = response;
})
.then(() => {
dispatch({
type: ActionKind.Add,
payload: newCard,
});
})
.catch((err) => {
console.log(`asyncAdd failed with the error \n ${err}`);
});
}, [catchCount]);
return (
<div>
<Deck deck={deck} />
<CatchButton onClick={()=>{setCatchCount(catchCount + 1)}>Catch Another</CatchButton>
</div>
);
};
So there are a couple of things with your code, but the last version is closest to being correct. Generally you want promise calls inside useEffect. If you want it to be called once, use an empty [] dependency array. https://reactjs.org/docs/hooks-effect.html (ctrl+f "once" and read the note, it's not that visible). Anytime the dep array changes, the code will be run.
Note: you'll have to change the calls to the Pokemon service as you're running two async calls without awaiting either of them. You need to make getRandomPokemon async and await both calls, then return the result you want. (Also you're returning newCard but not assigning anything to it in the call). First test this by returning a fake data in a promise like my sample code then integrate the api if you're having issues.
In your promise, it returns a Card which you can use directly in the dispatch (from the response, you don't need the extra step). Your onclick is also incorrectly written with the brackets. Here's some sample code that I've written and seems to work (with placeholder functions):
type Card = { no: number };
function someDataFetch(): Promise<void> {
return new Promise((resolve) => setTimeout(() => resolve(), 1000));
}
async function pokemonPromise(count: number): Promise<Card> {
await someDataFetch();
console.log("done first fetch");
await someDataFetch();
console.log("done second fetch");
return new Promise((resolve) =>
setTimeout(() => resolve({ no: count }), 1000)
);
}
const initialState = { name: "pikachu" };
const cardReducer = (
state: typeof initialState,
action: { type: string; payload: Card }
) => {
return { ...state, name: `pikachu:${action.payload.no}` };
};
//App.tsx
const App = () => {
const [deck, dispatch] = useReducer(cardReducer, initialState);
const [catchCount, setCatchCount] = useState(0);
useEffect(() => {
pokemonPromise(catchCount)
.then((newCard) => {
dispatch({
type: "ActionKind.Add",
payload: newCard
});
})
.catch((err) => {
console.log(`asyncAdd failed with the error \n ${err}`);
});
}, [catchCount]);
return (
<div>
{deck.name}
<button onClick={() => setCatchCount(catchCount + 1)}>
Catch Another
</button>
</div>
);
};

React : Empty values on PUT for axios

I have a simple list that I get from an API using axios.
Every element is a modifiable input, with it own update button.
After changing the data of an input, and while performing PUT request, console.log(test); returns empty values.
I checked console.log(newList); which is the array of the list, and the changing data are indeed happening in the list, but it seems they can't be sent to the server.
Note : The API is just for testing, the PUT method may not work, but atleast the values in the console should be sent.
Note2 : I don't know how to place the id of an item of the list in the url so you may encounter an error. / You can try with 1,2 or 3 instead for testing.
https://codesandbox.io/s/quizzical-snowflake-dw1xr?file=/src/App.js:1809-1834
import React, { useState, useEffect } from "react";
import axios from "axios";
export default () => {
const [list, setList] = React.useState([]);
const [name, setName] = React.useState("");
const [description, setDescription] = React.useState("");
const [city, setCity] = React.useState("");
// Getting initial list from API
useEffect(() => {
axios
.get("https://6092374385ff5100172122c8.mockapi.io/api/test/users")
.then((response) => {
setList(response.data);
console.log(response);
})
.catch((err) => console.log(err));
}, []);
// onUpdate to update the data in the API
const onUpdate = (e) => {
e.preventDefault();
const test = {
name: name,
description: description,
city: city
};
console.log(test);
// axios request PUT data on API
axios
.put(
"https://6092374385ff5100172122c8.mockapi.io/api/test/users" + id,
test
)
.then((res) => {
alert("success");
console.log(res);
})
.catch((error) => {
console.log(error);
});
// axios request GET to get the new modified list from the database, after the update
axios
.get("https://6092374385ff5100172122c8.mockapi.io/api/test/users")
.then((res) => {
alert("success");
console.log(res);
})
.catch((error) => {
console.log(error);
});
};
// Handler for changing values of each input
function handleChangeUpdate(id, event) {
const { name, value } = event.target;
const newList = list.map((item) => {
if (item.id === id) {
const updatedItem = {
...item,
[name]: value
};
return updatedItem;
}
return item;
});
setList(newList);
console.log(newList);
}
return (
<div>
<ul>
<div>
{list.map((item) => (
<li key={item.id}>
<input
className="form-control"
name="name"
onChange={(event) => handleChangeUpdate(item.id, event)}
defaultValue={item.name}
></input>
<input
className="form-control"
name="description"
onChange={(event) => handleChangeUpdate(item.id, event)}
defaultValue={item.description}
></input>
<input
className="form-control"
name="city"
onChange={(event) => handleChangeUpdate(item.id, event)}
defaultValue={item.city}
></input>
<button onClick={onUpdate}>Update</button>
</li>
))}
</div>
</ul>
</div>
);
};
It's because you never set the values of the props. That is why they never change from their initial values. You just update the list prop in handleChangeUpdate. There are two steps you need to take with the existing file structure:
Make handleChangeUpdate be able to differentiate between different props (city, description, etc.). For example, by passing the prop's name.
Update the prop's value in the handleChangeUpdate.
To realize the first step, you can change the input tag like the following:
{/* attention to the first argument of handleChangeUpdate */}
<input
className="form-control"
name="name"
onChange={(event) => handleChangeUpdate("name", item.id, event)}
defaultValue={item.name}
></input>
Then, you need to adjust the handleChangeUpdate:
if (name === "name") {
setName(value);
} else if (name === "description") {
setDescription(value);
} else if (name === "city") {
setCity(value);
}
By the way, list is not a good name for a variable.
Alternatively
Without creating new parameters, you can also use only the event to set the props
// Handler for changing values of each input
function handleChangeUpdate(id, event) {
const { name, value } = event.target;
const newList = list.map((item) => {
if (item.id === id) {
const updatedItem = {
...item,
[name]: value
};
return updatedItem;
}
return item;
});
setList(newList);
console.log(newList);
if (name === "name") {
setName(value);
} else if (name === "description") {
setDescription(value);
} else if (name === "city") {
setCity(value);
}
}
I think you have 3 errors in the onUpdate function.
You are not passing the id of the item from the onClick event
Your put method should be change
You should not perform get request as soon as after the put request, because sometimes the backend will not updated yet.
You can update your code as below,
1.Pass the id of the item when the button is clicked.
<button onClick={onUpdate(item.id)}>Update</button>
Modify the put method, passing the id
axios
.put(
`https://6092374385ff5100172122c8.mockapi.io/api/test/users/${e}`,
test
).then((res) => {
alert("success");
console.log(res);
})
.catch((error) => {
console.log(error);
});
3.Perform the get request after the response of the put request
const onUpdate = (e) => {
const test = {
name: name,
description: description,
city: city
};
console.log(test);
// axios request PUT data on API
axios
.put(
`https://6092374385ff5100172122c8.mockapi.io/api/test/users/${e}`,
test
)
.then((res) => {
console.log(res);
// axios request GET to get the new modified list from the database, after the update
axios
.get("https://6092374385ff5100172122c8.mockapi.io/api/test/users")
.then((res) => {
alert("success");
console.log(res);
})
.catch((error) => {
console.log(error);
});
})
.catch((error) => {
console.log(error);
});
};

Reactjs variable is returning with undefined after useEffect

I am very new to Reactjs, I am working on retrieving some data in order to display it, everything gets displayed however, when I filter there is an error that comes up "Cannot read property 'filter' of undefined", after debugging I found out that dataList is returning with undefined when typing anything in the search bar.
Appreciate your assistance.
function App() {
var dataList;
useEffect(() => {
// http get request
const headers = {
'Content-Type': 'application/json',
'Authorization': '***********************',
'UserAddressId': ****,
'StoreId': *
}
axios.get('https://app.markitworld.com/api/v2/user/products', {
headers: headers
})
.then((response) => {
dataList = response.data.data.products
setData(dataList)
})
.catch((error) => {
console.log(error)
})
}, []);
const [searchText, setSearchText] = useState([]);
const [data, setData] = useState(dataList);
// exclude column list from filter
const excludeColumns = ["id"];
// handle change event of search input
const handleChange = value => {
setSearchText(value);
filterData(value);
};
// filter records by search text
const filterData = (value) => {
console.log("dataList", dataList)
const lowercasedValue = value.toLowerCase().trim();
if (lowercasedValue === "") setData(dataList);
else {
const filteredData = dataList.filter(item => {
return Object.keys(item).some(key =>
excludeColumns.includes(key) ? false :
item[key].toString().toLowerCase().includes(lowercasedValue)
);
});
setData(filteredData);
}
}
return (
<div className="App">
Search: <input
style={{ marginLeft: 5 }}
type="text"
placeholder="Type to search..."
value={searchText}
onChange={e => handleChange(e.target.value)}
/>
<div className="box-container">
{data && data.length > 0 ? data.map((d, i) => {
return <div key={i} className="box">
<b>Title: </b>{d.title}<br />
<b>Brand Name: </b>{d.brand_name}<br />
<b>Price: </b>{d.price}<br />
<b>Status: </b>{d.status}<br />
</div>
}) : "Loading..."}
<div className="clearboth"></div>
{data && data.length === 0 && <span>No records found to display!</span>}
</div>
</div>
);
}
export default App;
You're mixing up a stateful data variable with a separate non-stateful, local dataList variable. The dataList only gets assigned to inside the axios.get, so it's not defined on subsequent renders; the setData(dataList) puts it into the stateful data, but the dataList on subsequent renders remains undefined.
To make things easier to understand, remove the dataList variable entirely, and just use the stateful data.
You also probably don't want to discard the existing data when the user types something in - instead, figure out what items should be displayed while rendering; rework the filterData so that its logic is only carried out while returning the JSX.
const [searchText, setSearchText] = useState([]);
const [data, setData] = useState([]);
useEffect(() => {
// http get request
const headers = {
'Content-Type': 'application/json',
'Authorization': '***********************',
'UserAddressId': ****,
'StoreId': *
}
axios.get('https://app.markitworld.com/api/v2/user/products', {
headers: headers
})
.then((response) => {
setData(response.data.data.products);
})
.catch((error) => {
console.log(error)
})
}, []);
// handle change event of search input
const handleChange = value => {
setSearchText(value);
};
// filter records by search text
const filterData = () => {
const lowercasedValue = searchText.toLowerCase().trim();
return lowercasedValue === ""
? data
: data.filter(
item => Object.keys(item).some(
key => excludeColumns.includes(key) ? false :
item[key].toString().toLowerCase().includes(lowercasedValue)
)
);
}
And change
{data && data.length > 0 ? data.map((d, i) => {
to
{filterData().map((d, i) => {
Your searchText should also be text, not an array: this
const [searchText, setSearchText] = useState([]);
should be
const [searchText, setSearchText] = useState('');
First of all, you don't need to maintain an additional non-state variable dataList as the local state data would serve the purpose.
API Call Code:
You should directly store the response from API after null checks satisfy.
useEffect(() => {
const headers = {
// key value pairs go here
};
// http request
axios.get(endPoint, {
headers,
})
.then((response) => {
// set data directly null checks
setData(response.data.data.products);
})
.catch((error) => {
console.log(error);
});
}, []);
Filteration Code
Use useCallback hook which would return a memoized version of the callback, unless the value of data changes.
const filterData = useCallback((value) => {
console.log('data', data);
// rest of code
}, [data]);

React update sibling component

I'm confused about react hot updating components.
I've got something like this:
const SingleEvent = ({ event }) => (
<>{event.status}</>
)
const EventDetails = ({ event, updateEvent }) => (
<button
onClick={async () => {
const data = await getAPIResponse(); // { status: 'open' }
updateEvent(event.id, data)
}
>
Update
</button>
)
const List = ({ events, updateEvent, selectedEvent }) => {
if (selectedEvent) {
return <EventDetails event={selectedEvent} updateEvent={updateEvent} />
}
return (
<>
{events.map(event => <SingleEvent event={event}/>)}
</>
)
}
const Page = ({ initialEvents }) => {
const [events, setEvents] = useState(initialEvents || []);
const [selectedEvent, setSelectedEvent] = useState(null);
const updateEvent = (eventId, data) => {
setEvents(prevState => {
const eventIndex = prevState.findIndex(
element => element._id === eventId,
);
if (eventIndex === -1) {
return prevState;
}
prevState[eventIndex] = {
...prevState[eventIndex],
...data,
};
return prevState;
});
};
return <List events={events} updateEvent={updateEvent} selectedEvent={selectedEvent} />
}
In the <EventDetails /> component I'm updating one of the events (basically changing it's status). If API works fine, when I close the details (set the selectedEvent to null) everything is changing as it should. If I close the the details before getting the API response - nothing changed.
I've checked the updateEvent function, and it's performing the update, but the UI is not refreshed.
To be clear:
I Open the <EventDetails /> component, I'm pressing the button to update the event. The API should change its status. When I close the EventDetails I'm getting a List of <SingleEvent /> components. Every one of them displays the event.status.
If I close the EventDetails before getting response, status in SingleEvent is not updating. If I wait for the response everything works ok.
Since the component is unmounted before the data is fetched, its no longer able to update the response.
You can instead provide a function as props which performs the API requst and updates the status
const EventDetails = ({ handleClick }) => (
<button
onClick={handleClick}
>
Update
</button>
)
const List = ({ events, updateEvent, selectedEvent }) => {
const handleClick = async () => {
const data = await getAPIResponse(); // { status: 'open' }
updateEvent(event.id, data)
}
if (selectedEvent) {
return <EventDetails event={selectedEvent} updateEvent={updateEvent} handleClick={handleClick}/>
}
return (
<>
{events.map(event => <SingleEvent event={event}/>)}
</>
)
}
The problem was with the update method. Operating directly on prevState is not a good idea. After I changed the updateEvent function, everything works fine.
setEvents(prevState =>
prevState.map(event => {
if (event._id === eventId) {
return {
...event,
...updatedEvent,
};
}
return event;
})
);

Categories