Why is my cached state undefined on mutate in SWR? - reactjs

I have a component that gets the data for a subreddit with posts and every post has a vote:
const subUrl = `/api/subreddit/findSubreddit?name=${sub}`;
const { data: fullSub, error } = useSWR(subUrl, fetchData, {
initialData: props.fullSub, //comes from next.js getServerSideProps
});
This works perfectly fine and it renders everything on the screen.
But on my subredditPost component (I render a subredditPost for each post) I have this onClick function when I click the upvote button:
const upvotePost = async (postid, fullSub, subUrl) => {
console.log("the sub variable is: ", subUrl); //same url as in the main component
mutate(
subUrl,
async (data) => {
// this is undefined when I hit on the upvote button
// Expected: The current state of swr with the key of subUrl
console.log("cached value: ", data);
return {
...data,
posts: data.posts.map((post) => {
if (post.id === postid) {
return {
...post,
votes: [...post.votes, { voteType: "UPVOTE" }],
};
} else {
return post;
}
}),
};
},
false
);
const res = await fetch("/api/post/upvote", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ postId: postid }),
});
mutate(subUrl);
};
It always is undefined in the console.log on the code above. So I get an error and nothing renders on the page anymore. What am I doing wrong? I read it can't get the cache if I use a different key, but I'm using the same key. I pass the subUrl from my main component (where I fetch the subreddit) to the post component (where I click the upvote button). So it is exactly the same.
Why do I get undefined?

I was able to solve this issue by providing a default value to mutate like this:
mutate(
sub,
async (data = fullSub) => {
console.log("cached value: ", data);
return {
...data,
posts: data.posts.map((post) => {
if (post.id === postid) {
return {
...post,
votes: [...post.votes, { voteType: "UPVOTE" }],
};
} else {
return post;
}
}),
};
},
false
);
So give the async function a default value if it's undefined: async (data = fullSub)
That fixes it but it's still not clear why it's undefined? Maybe somebody can answer this. I will leave this open for now, after a while if nobody has a better answer I will mark mine.

Related

React-query useInfiniteQuery: getNextPageParam not working

I'm stuck using useInfiniteQuery.
The first call works fine, but the next page is not called with getNextPageParam
const getProductItems = async (par) => {
console.log("axios :", par);
const res = await axios.get(`/api/v1/products`, {
params: par,
});
return {
result: res.data,
};
};
export default function useGetProductItems(params) {
const { data, isLoading, fetchNextPage, hasNextPage, isFetching } =
useInfiniteQuery(
["getItems"],
({ pars = params }) => getProductItems(pars),
{
getNextPageParam: (res) => {
console.log(res);
const nextParams = {
...res.result.pageInfo,
page: res.result.pageInfo.page + 1,
};
console.log("next :", nextParams);
return nextParams;
},
select: (data) => {
return data.pages[0].result.data;
},
}
);
return {
data,
isLoading,
fetchNextPage,
hasNextPage,
isFetching,
};
}
And the Query Client setting is like this
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
queryCache: new QueryCache({
onError: errorHandler,
}),
mutationCache: new MutationCache({
onError: errorHandler,
}),
});
As I am new to react-query, I am also wondering if there is any data that must be received from the API.
plz answer for me
You can access pageParam and send it as argument to your fetching function. Also it'd be a good idea to check if there really is a next page before incrementing the actual page number in getNextPageParam. Something like this:
const { data, isLoading, fetchNextPage, hasNextPage, isFetching } =
useInfiniteQuery(
['getItems'],
({ pageParam = 1 }) => getProductItems(pageParam), // pageParam defaults to the first page
{
getNextPageParam: lastPage => {
return lastPage.result.pageInfo.page < lastPage.result.pageInfo.totalPages // Here I'm assuming you have access to the total number of pages
? lastPage.result.pageInfo.page + 1
: undefined // If there is not a next page, getNextPageParam will return undefined and the hasNextPage boolean will be set to 'false'
},
select: data => {
return data.pages[0].result.data
},
}
)
I don't have information about how is your API endpoint built, but typically the request should look, for example, like this:
const getProductItems = async (page) => {
const res = await axios.get(`/api/v1/products?page=${page}`);
return {
result: res.data,
};
};

How to perform action or render component before/after every http request in React?

I want to know if there is a way to create a kind of middleware in React?
What i want is to have an alert component show if there is a failing result for an http request.
Right now, i am making http request on login,registration,etc and i am importing my alert component in every page and setting the Alert component props like type, message, visibility everywhere i need the component, but i think maybe there is a better way of doing this.
Here is my code:
...imports
export const RegisterPage = () => {
const [alertConfig, setAlertConfig] = useState({
type: "",
message: "",
show: false,
});
...code
const onSubmitHandler = async (e) => {
e.preventDefault();
if (!isFormValid()) return;
const formData = new FormData();
formData.append("password", formValues.password);
if (formValues.provider.startsWith("8")) {
formData.append("contact", formValues.provider);
} else {
formData.append("email", formValues.provider);
}
setIsLoading(true);
try {
const response = await fetch(
`${process.env.REACT_APP_API_URL}/auth/register`,
{
method: "POST",
body: formData,
}
);
const data = await response.json();
if (data.status === "success") {
const { token, user } = data.data;
dispatch(setCurrentUser(user, token));
navigate("/choose-actor");
} else {
setAlertConfig({
type: "warning",
message: data.message,
show: true,
});
}
} catch (error) {
console.log(error);
setAlertConfig({
type: "danger",
message: "Ocorreu algum erro",
show: true,
});
} finally {
setIsLoading(false);
}
};
return
...html
{alertConfig.show && <Alert {...alertConfig} />}
...more html
As you can see, i am changing the configuration for the alert inside inside the function that executes the http request, and i have to do the save for every page that performs this action.
I looking for a design patter where i dont have to repeat myself.
Hope my question is clear.

Fetch not working after switch from class to functional component

I'm converting some class components to functional.
Console.log(data) returns the expected output but then once i try to set it using useState and check the value, it returns an empty array.
On the class component its working by using the state.
Functional Component ( Not Working )
const [submitting, setSubmitting] = useState(false)
const [watingForResult, setWaitingForResult] = useState(false)
const [submission, setSubmission] = useState([]);
const [scoringResults, setScoringResults] = useState([]);
+
function submitSubmission() {
setSubmitting(true);
setResult([]);
setError('');
let data = {
code: btoa(code),
language: { name: language.name },
users: { username: parseLocalJwt().username },
problem: { name: textToLowerCaseNoSpaces(problem.name) }
}
fetch(URL + '/submission', {
method: 'POST',
body: JSON.stringify(data),
headers: new Headers({
...getAuthorization(),
'Content-Type': 'application/json'
})
}).then(res => res.json()).then(data => {
setSubmitting(true);
setWaitingForResult(true);
setSubmission(data);
console.log('submitSubmission' + JSON.stringify(data));
window.secondsWaiting = new Date().getTime();
window.resultsListener = setInterval(fetchForResults(data.id), 1000);
});
}
...
function fetchForResults() {
console.log('data on fetchForResults' + submission.id)
}
...
<button
type="button"
onClick={submitSubmission}>
Submit!
</button>
console.log screenshot
Class component ( Working )
super(props);
this.state = {
problem: [],
sentSubmission: {
submitting: false,
waitingForResults: false,
submission: [],
scoringResults: []
},
results: {
loaded: false,
result: [],
error: ''
},
language: { mode: 'java', name: 'Java' },
code: ``
}
submitSubmission() {
this.setState({ sentSubmission: { submitting: true }, results: { result: [], error: '' } })
let data = {
code: btoa(this.state.code),
language: { name: this.state.language.name },
users: { username: parseLocalJwt().username },
problem: { name: textToLowerCaseNoSpaces(this.state.problem.name) }
}
fetch(URL + '/submission', {
method: 'POST',
body: JSON.stringify(data),
headers: new Headers({
...getAuthorization(),
'Content-Type': 'application/json'
})
}).then(res => res.json()).then(data => {
this.setState({ sentSubmission: { submitting: true, waitingForResults: true, submission: data } })
window.secondsWaiting = new Date().getTime();
window.resultsListener = setInterval(this.fetchForResults, 1000);
});
}
Some portion of the code is missing but, since you switched from a class component's state to a functional component state, I guess the issue is related to how you are using your state.
In functional components, when you set your state, you are changing the whole object in your state. So, when in your code you do setSentSubmission({ submitting: true });, your state becomes:
previousState = {
submitting: false,
waitingForResults: false,
submission: [],
scoringResults: []
}
nextState = {
submitting: true
//you lost watingForResults, submissions and scoring results
}
When using useState it is always suggested to decompose your object in different states:
const [submitting, setSubmitting] = useState(false)
const [watingForResult, setWaitingForResult] = useState(false)
//and so on...
//And then update them singularly:
//This...
setSentSubmission({ submitting: true });
//... become this
setSubmitting(true);
Finally, side note on lists. If you need to add a single element to a list in your state you can do:
setListState(currentList => [...currentList, newElement])
If this approach doesn't fit your use case and you need a more complex state management system I suggest you to look at useReducer (link).
Update
To print the content of your fetch you have either 2 choices.
Print directly the content retrieved in your fetch function
fetch('http://myendpoint.com/mydata.json')
.then(response => response.json())
.then(data => console.log(data));
If you need to do some processing to your data and you want to print what is the content of your updated state you can just use a useEffect hook. For instance, if you want to print the content of your scoringResult state you just write:
useEffect(() => {
console.log(scoringResult)
},[scoringResult])
This hook will be triggered every time scoringResult is updated (plus once when the component was mounted in the beginning)
Problem with your code is this:
setSentSubmission(...)
console.log(data);
console.log(sentSubmission.submission)
You can not set state and then to expect immediately to access that new state, new state can be accessed after component rerenders(in next iteration), all this is because you are reading sentSubmission.submission from closure which will be recreated only when component rerenders. So everything works fine with you code, you are just doing logging in a wrong place, move that log outside of the submitSubmission and you will see that state is updated successfully and that log will be printed after component rerenders(state updates).
When function submit() call, you set new value for sentSubmission.
Try this instead:
setSentSubmission((prevState) => (…prevState, { submitting: true }));

Setting up proper MongoDB implementation in React app

So, currently I'm working on internship React (MERN) app, which is a simple to-do list with ability to create, delete and edit todos. I will post some code from it, but you also can look at the full code on GitHub: https://github.com/Wonderio619/magisale-internship-todo
The next task is connecting my app to MongoDB. I have some "boilerplate" code - I alredy set up connection with MongoDB, also have Express router with routes like get all todos list, send todo to database, update todo with id, get todo with id:
const express = require("express");
const router = express.Router();
let Todo = require('../models/model')
// get all todo list with id
router.get('/', function (req, res) {
Todo.find()
.then((todos) => res.json(todos))
.catch((error) => res.send(error))
})
// send todo to database
router.post('/', function (req, res) {
let todo = new Todo();
todo.titleText = req.body.title;
todo.todoText = req.body.body;
todo.save(function (err) {
if (err)
res.send(err);
res.send('Todo successfully added!');
});
})
// get todo with id
router.get('/:todoId', function (req, res) {
Todo.findById(req.params.todoId)
.then(foundTodo => res.json(foundTodo))
.catch(error => res.send(error));
})
// updates todo with id
router.put('/:todoId', function (req, res) {
Todo.findOneAndUpdate({ _id: req.params.todoId }, req.body, { new: true })
.then((todo) => res.json(todo))
.catch((error) => res.send(error))
})
// deletes todo with id
router.delete('/:todoId', function (req, res) {
Todo.remove({ _id: req.params.todoId })
.then(() => res.json({ message: 'todo is deleted' }))
.catch((error) => res.send(error))
})
module.exports = router;
These routes used when corresponding methods from todo app are called:
import React, { Component } from 'react';
import './ToDo.css';
import Logo from './assets/logo.png';
import ToDoItem from './components/ToDoItem';
import AppBar from './components/AppBar';
import Popover from './components/Popover';
import { connect } from 'react-redux';
class ToDo extends Component {
constructor(props) {
super(props);
this.state = {
list: [],
title: '',
todo: '',
};
};
componentDidMount = () => {
fetch("/api/todos")
.then(data => data.json())
.then(res => this.setState({ list: res.data }));
console.log(this.state.list)
};
createNewToDoItem = () => {
fetch("/api/todos", {
method: "post",
headers: new Headers({
"Content-Type": "application/json"
}),
body: JSON.stringify({
title: this.state.title,
body: this.state.todo
})
})
.catch(err => {
console.error(err);
});
if (this.state.title !== '' & this.state.todo !== '') {
this.props.createTodoItem(this.state.title, this.state.todo);
this.setState({ title: '', todo: '' });
}
};
handleTitleInput = e => {
this.setState({
title: e.target.value,
});
};
handleTodoInput = e => {
this.setState({
todo: e.target.value,
});
};
editItem = (i, updTitle, updToDo) => {
const modifyURL = "/api/todos/" + i;
fetch(modifyURL, {
method: "put",
headers: new Headers({
"Content-Type": "application/json"
}),
body: JSON.stringify({
title: updTitle,
todo: updToDo
})
})
.then(resp => {
if (!resp.ok) {
if (resp.status >= 400 && resp.status < 500) {
return resp.json().then(data => {
let error = { errorMessage: data.message };
throw error;
});
} else {
let error = {
errorMessage: "Please try again later. Server is not online"
};
throw error;
}
}
return resp.json();
})
.then(newTodo => {
let arr = this.props.list;
arr[i].title = updTitle;
arr[i].todo = updToDo;
this.setState({ updateList: true });
});
};
deleteItem = indexToDelete => {
const deleteURL = "/api/todos/" + indexToDelete;
fetch(deleteURL, {
method: "delete"
})
.then(resp => {
if (!resp.ok) {
if (resp.status >= 400 && resp.status < 500) {
return resp.json().then(data => {
let error = { errorMessage: data.message };
throw error;
});
} else {
let error = {
errorMessage: "Please try again later. Server is not online"
};
throw error;
}
}
return resp.json();
})
.then(() => {
this.props.deleteTodoItem(indexToDelete);
});
};
randId() {
return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(2, 10);
}
eachToDo = (item, i) => {
return <ToDoItem
key={this.randId()}
title={item.title}
todo={item.todo}
deleteItem={this.deleteItem.bind(this, i)}
editItem={this.editItem.bind(this, i)}
/>
};
render() {
const { list } = this.props;
return (
<div className="ToDo">
<img className="Logo" src={Logo} alt="React logo" />
<AppBar />
<div className="ToDo-Container">
<div className="ToDo-Content">
{list.map(this.eachToDo)}
</div>
<div>
<Popover
toDoValue={this.state.todo}
titleValue={this.state.title}
titleOnChange={this.handleTitleInput}
toDoOnChange={this.handleTodoInput}
addHandler={this.createNewToDoItem}
/>
</div>
</div>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
list: state.list
}
}
const mapDispatchToProps = dispatch => {
return {
deleteTodoItem: id => {
dispatch({ type: "DELETE_TODO", id: id });
},
createTodoItem: (title, todo) => {
dispatch({ type: "CREATE_TODO", title: title, todo: todo });
}
};
};
export default connect(mapStateToProps, mapDispatchToProps)(ToDo);
Note that "list" array from state is not really used, bacause I have initial list state in Redux state here( it may be implemented bad, but it is anyway):
const initState = {
list: [
{
title: 'Cup cleaning',
todo: "Wash and take away the Kurzhiy's cup from WC"
},
{
title: 'Smoking rollton',
todo: 'Do some rollton and cigarettes'
},
{
title: 'Curious dream',
todo: 'Build a time machine'
}
],
};
const rootReducer = (state = initState, action) => {
switch (action.type) {
case "DELETE_TODO":
let newList = state.list.filter((todo, index) => action.id !== index)
return {
...state,
list: newList
}
case "CREATE_TODO":
const title = action.title;
const todo = action.todo;
let createdList = [
...state.list,
{
title,
todo
}
]
return {
...state,
list: createdList
}
default:
return state;
}
}
export default rootReducer;
So, now I need some help - if I understand everything right, my list state should now be stored inside MongoDB database. But currently it is in Redux, how should I switch from current state implementation to MongoDB properly ?
Also I understand that my MongoDB implementation is far from perfection, I'm just newbie to this, but I need to solve following problems:
1) I tried to get all todos from database in ComponentDidMount method and save it in array, but console.log always show that array is empty smth definitely wrong there.
2) Also connection with database is not really set up, because in general I can only add todos to database, but delete or edit functions does not work, because I'm little stuck about how to implement this index stuff, should I use ObjectId property from MongoDB or should I pass indexes from my main component to database, and how ?
Also any global recommendations regarding proper mongodb implementaion and suggestions or fixes to my code will be greatly appreciated :)
It's not res.data but res that you should inject in your state. res.data is undefined so it won't update the state.list.
componentDidMount = () => {
fetch("/api/todos")
.then(data => data.json())
.then(jsonData => {
console.log('jsonData --', jsonData)
console.log('jsonData.data is empty!', jsonData.data)
this.setState({ list: jsonData })
});
};
1- To be able to update, you're sending an id. You may create id's in your db if that's the way you want to find your todos.
Please note that _id is different from id.
_id mongodb's ObjectId, it is not of type integer but of type ObjectId.
id is just a regular field that you created that is called id.
NB: Your req.params.todoId is an integer. While ObjectId is of type ObjectId! So you won't be able to query one with the wrong type.
var todoSchema = new Schema({
id: Number,
titleText: String,
todoText: String
});
2- Get your todo and update it thanks to the id. If it does not exist, it will be created thanks to the upsert option. Don't forget to cast in order to match your schema. title: req.body.title won't work because you defined it as titleText in your schema.
// updates todo with id
router.put('/:todoId', function (req, res) {
const data = {
titleText: req.body.title,
todoText: req.body.todo
}
Todo.findOneAndUpdate(
{ id: req.params.todoId }, // the query
{ $set: data }, // things to update
{ upsert: true } // upsert option
).then((todo) => res.json(todo))
.catch((error) => res.send(error))
})

trying to NOT get response one change late

So I have an input :
onChange this input's value get's updated and stored in props for persistance but that's unrelated to the issue.
I'm going to add a checkmark next to the field if the entered value is found in the database.
I already have the call to API and answer set up (my redux reducer) :
import EventTypes from '../EventTypes';
const initialState = {
response: {},
};
export default (state = initialState, action) => {
switch (action.type) {
case EventTypes.EXISTS_FULFILLED.type:
console.log(action.payload.body().data());
return { ...state, response: action.payload.body().data() };
default:
return state;
}
};
Note that the above console log correctly prints the true/false value (+ sent value. I get the two back from the server).
but in the component that calls this :
const defaultProps = {
onChange: () => {
},
value: { type: '' },
provider: { type: '' },
};
class InputsComponent extends Component {
onChange = () => {
this.props.onChange(this.props.id);
};
updateFirstInput(event) {
const { onChange, exists, response } = this.props;
const payload = {
objectType: this.props.placeholder.toLowerCase(),
objectName: event.target.value,
};
exists(payload);
this.setState({ firstInput: event.target.value });
onChange({ value: {
...this.state,
firstInput: event.target.value,
} }, this.props.id);
setTimeout(() => {
this.setState({ exitsStore: response });
}, 200);
setTimeout(() => {
console.log(this.state.exitsStore);
}, 1000);
}
The console logs are empty the first time "updateFirstInput()" is called and then are always one "onChange" behind from being accurate.
that is for "hello" :
[input value] [ console log of value for which presence on the server is true/false]
"h" ""
"he" "h"
"hel" "he"
"hell" "hel"
"hello" "hell"
after awhile I figured out that this was happening.
I've tried this :
const defaultProps = {
onChange: () => {
},
value: { type: '' },
provider: { type: '' },
};
class InputsComponent extends Component {
onChange = () => {
this.props.onChange(this.props.id);
};
checkExists(object){
const { exists, response } = this.props;
const payload = {
objectType: this.props.placeholder.toLowerCase(),
objectName: object,
};
exists(payload);
return response;
}
updateFirstInput(event) {
const { onChange } = this.props;
this.setState({ firstInput: event.target.value });
onChange({ value: {
...this.state,
firstInput: event.target.value,
} }, this.props.id);
const response = await this.checkExists(event);
console.log(response);
this.setState({ exitsStore: response });
console.log('stored data :');
console.log(this.state.exitsStore);
}
but for some reason people online suggesting we use await are not providing context. the above is not even compilable. (npm start will fail)
and even before that ESLint indicates that "await is a reserved word expecting newline or semicolon".
but I'm quite sure the above even with correct syntaxe wouldn't work either.
it's probably not await but how do I manage to have a filled in response from server (ideally) within a single function, the same function that calls the input update.
Constrainst :
as far as I know you can't have both onBlur and onChange on the same input so since i'm already using onChange no onBlur.
and I can't call any of this within a parent or other : there are multiple input fields as children and I need to have the value check and checkmark appear actions happen within the child to be able to match the values together.
UPDATE :
and if I simply remove the timers :
const defaultProps = {
onChange: () => {
},
value: { type: '' },
provider: { type: '' },
};
class InputsComponent extends Component {
onChange = () => {
this.props.onChange(this.props.id);
};
updateFirstInput(event) {
const { onChange, exists, response } = this.props;
const payload = {
objectType: this.props.placeholder.toLowerCase(),
objectName: event.target.value,
};
exists(payload);
this.setState({ firstInput: event.target.value });
onChange({ value: {
...this.state,
firstInput: event.target.value,
} }, this.props.id);
this.setState({ exitsStore: response });
console.log(this.state.exitsStore);
}
...TWO calls behind.
setState is asynchronous. It looks like you are using timeouts to try to get around this fact, but that's a very crude and brute force way to "solve" the problem and it doesn't actually solve the problem (as shown by your results).
But setState has handlers available when you need to run code "after" it. You do this simply by passing in a callback function as the second argument. E.g.;
this.setState({
exitsStore: response
}, () => {
console.log(this.state.exitsStore);
});
The handy pre-existing methods of React to the rescue.
In this case : componentWillRecieveProps().
componentWillReceiveProps(newProps) {
const response = newProps.response;
const old = this.props.response;
console.log(response);
const id = this.props.id;
if (response !== old && response.objectIdentifier === id) {
if (response.activ) {
if (response.isthere) {
this.setState({ inputIcon: 4 });
} else {
this.setState({ inputIcon: 3 });
}
} else {
this.setState({ inputIcon: 2 });
}
}
}
onChange = () => {
this.props.onChange(this.props.id);
};
updateInput(event) {
const { onChange, exists, response } = this.props;
const inputValue = event.target.value;
this.setState({ input: inputValue });
onChange({ value: {
...this.state,
input: inputValue,
} }, this.props.id);
if (inputValue === '') {
this.setState({ inputIcon: 0 });
} else {
const placeHolder = this.props.placeholder.toLowerCase();
const objectIdentifier = this.props.id;
const payload = {
objectType: placeHolder,
objectName: inputValue,
objectIdentifier,
};
exists(payload);
this.setState({
existStore: response,
};
}
}
the reason why this works is that componentWillRecieveProps() by default receives a parameter which is the updated props state and this one is indeed the new one. you can check that there has indeed been an update by doing as I did : checking the object you have in props against the passed parameter and only perform your actions if they are different.
I need edits on this post please, because I'm terrible at expressing this with the correct developer terms!

Resources