How to set state based on different functions in react using firebase - reactjs

I am trying to make a template system in my survey tool and set the state to a json object which will be pushed onto the firebase database, but I want to know how to change the state based on which template i choose in the previous page:
I have 4 templates:
Blank
Board
Board Member
Chairperson
I am showing these as buttons and when any of them is clicked the corresponding handlers run
<button
onClick={function(event) {
props.clicked();
notify();
}}
className={classes.Blank}
>
Blank Survey
</button>
<button onClick={props.board} className={classes.Board}>
Board Survey
</button>
<button onClick={props.chair} className={classes.Chair}>
Chairperson Survey
</button>
<button onClick={props.member} className={classes.Member}>
Board Member Survey
</button>
Now I use the props to assign them to corresponding handlers:
<NewSurveyView
clicked={this.clickHandler}
board={this.boardHandler}
chair={this.chairHandler}
member={this.memberHandler}
isLoading={this.props.loading}
error={this.props.isError}
/>
These are the three functions I am using to fetch the templates:
const fetchBoardMember = async () => {
const res = await axios.get(
"/surveys/ckb0kmt3n000e3g5rmjnfw4go/content/questions.json"
);
return res.data;
};
const fetchBoard = async () => {
const res = await axios.get(
"/surveys/ckb24goc800013g5r7j8igrk3/content/questions.json"
);
return res.data;
};
const fetchChair = async () => {
const res = await axios.get(
"/surveys/ckb24gt3s00053g5rrf60353r/content/questions.json"
);
return res.data;
};
Next, I create an object with all the info which i want to push (this is for Board Template):
const board = {
title: "Your Survey Title",
subTitle: "Your Survey Description",
creatorDate: new Date(),
lastModified: new Date(),
questions: fetchBoard().then(questions => {
this.setState(state => ({
...state,
content: {
...state.content,
questions
}
}));
}),
submitting: true
};
Setting the state:
this.setState({
_id: newId(),
content: board,
userId: this.props.user_Id
});
Now my question is, how do I change the variable used based on the template chosen by using the buttons?

Related

Reactjs updated prop is not shown

I tried to create a interactable map following this example here: https://docs.mapbox.com/mapbox-gl-js/example/cluster/
In my componentDidMount (where I create a mapboxgl) I implemented clickable markers, when clicked on the markers a popup appears which displays various informations.
After the click I want to call a second function (fetch) to get more data on that specific marker: this.props.getData(id);
I then want to display these data in the same popup as the other information.
My problem is that this.props.testdata is empty on the first click. If I double-click on the marker, the data appear. So my guess is that my component does not notice the change of the state/prop and therefore does not update?
How do I do that or what am I missing?
Map.js
this.map.on('click', 'unclustered-point', (e) => {
const coordinates = e.features[0].geometry.coordinates.slice();
const id = e.features[0].properties.id;
const infos = e.features[0].properties.infos;
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
}
if (id == null) {
console.log("Missing id, cant get informations")
return;
}
this.props.getData(id);
new mapboxgl.Popup()
.setLngLat(coordinates)
.setHTML(
`
Id: ${id}
<br>
Infos: ${infos}
<br>
<br>
Testdata: ${this.props.testdata}
`
)
.addTo(this.map);
});
this.map.on('mouseenter', 'clusters', () => {
this.map.getCanvas().style.cursor = 'pointer';
});
this.map.on('mouseleave', 'clusters', () => {
this.map.getCanvas().style.cursor = '';
});
});
App.js (getData function):
getData = (id) => {
if (id== null) {
console.log("Missing id")
return;
}
const {mapCenter, startDate, endDate} = this.state;
const neo4j = require('neo4j-driver')
const driver = neo4j.driver('bolt://xxx', neo4j.auth.basic("xx", "xx-xx"))
const session = driver.session()
session
.run('Here goes a neo4j cypher statment',{id: id})
.then((results)=> {
const data= [];
results.records.forEach((record) => data.push([record.get("r"), record.get("n"), record.get("b")]))
this.setState({
data
});
session.close()
driver.close()
}).catch(e => {
console.log(e)
session.close();
});
};
I am not familiar with neo4j, but it is apparent that getData(id) fetches data from a server. This is going to be an asynchronous operation, so you should add a state property to maybe show a spinner while data is being fetched?
Regarding testdata not being available, I do not see the code where it is being set.
Maybe your setState code should be:
this.setState({
testdata: data
});
//If your data prop is testdata.
As per the current setState, data property of your component state would be set with server response.
Updates:
Temporary fix for async server call:
You can change following methods and try if it fixes your issue:
this.map.on('click', 'unclustered-point', async (e) => {
// ...previous code
await this.props.getData(id);
// This forces the following code to execute synchronously. Basically it should wait for your API call to be complete
new mapboxgl.Popup()
.setLngLat(coordinates)
.setHTML(
`
Id: ${id}
<br>
Infos: ${infos}
<br>
<br>
Testdata: ${this.props.testdata}
`
)
.addTo(this.map);
});
this.map.on('mouseenter', 'clusters', () => {
this.map.getCanvas().style.cursor = 'pointer';
});
this.map.on('mouseleave', 'clusters', () => {
this.map.getCanvas().style.cursor = '';
});
});
getData = (id) => {
//... previous code
// we return a promise to use await in the onClick handler
return session
.run('Here goes a neo4j cypher statment',{id: id})
.then((results)=> {
const data= [];
results.records.forEach((record) => data.push([record.get("r"), record.get("n"), record.get("b")]))
this.setState({
data
});
session.close()
driver.close()
}).catch(e => {
console.log(e)
session.close();
});
}
If you are still facing an issue, please create a sample app and share.
I have not yet managed to fix the original problem.
However, I have found another solution:
In my Map.js I'm calling the this.props.testdata in th UI like this:
<div className="sidebar">
info: {JSON.stringify(this.props.testdata)}
</div>

How do I add some state to redux state when try to write unit test in jest/RTL?

I wanted to start testing with Redux-toolkit, according to the article I found.
https://redux.js.org/usage/writing-tests#setting-up
The right practice is to write integration test. But right now I want to test a sign out button which is controlled by authstate,in order to set it's value I have to sign in first. What I want to do is I can give some state to the authstate in the test file instead of having to login. So I can actually write unit test on my sign out button.
Here's the code and the test
const Navbar = () => {
const cart = useAppSelector((state) => state.cart);
const user = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();
const handleLogout = () => {
localStorage.removeItem("persist:root");
window.location.reload();
};
return(
{user.user !== null ? (
<>
<MenuItem>Hello {user.user?.username} </MenuItem>{" "}
<ExitToApp
style={{ cursor: "pointer", marginLeft: "10px" }}
onClick={() => handleLogout()}
/>
<Link to="/order">
<MenuItem>Order</MenuItem>
</Link>
</>
) : (
<>
<Link to="/register">
<MenuItem>REGISTER</MenuItem>
</Link>
<Link to="/login">
<MenuItem>SIGN IN</MenuItem>
</Link>
</>
)}
)
Authslice
const slice = createSlice({
name: "auth",
initialState: { user: null, token: null } as {
user: null | UserDataInterface;
token: null | string;
},
reducers: {
setCredentials: (state,{ payload: { user, token } }: PayloadAction<{ user: UserDataInterface; token: string }>) => {
state.user = user;
state.token = token;
}
},
extraReducers: (builder) => {}
});
test file
test("When click on logoutk,it will trigger handle logout", () => {
//TODO: should let the user state to not be empty first
await store.dispatch(setCredentials())
//TODO: then we can track if logout label exist
//TODO: click on logout button and mock localstorage maybe ?
});
What should I do with this kind of unit test, if it involves prerequisites for redux-state ?
After some research, I found out how to do this. It might not be the best practice. But I think it could be useful in a lot of scenario if you don't want to write integration test.
test("When click on logout,it will trigger handle logout", async () => {
//TODO: should let the user state to not be empty first
store.dispatch(
setCredentials({
user: {
username: "Kai",
_id: "efekfjefke",
email: "dfdkdfkdf@gmail.com",
createdAt: new Date(2013, 13, 1),
updatedAt: new Date(2013, 13, 1),
img: "223232",
},
token: "test12345",
})
);
//TODO: then we can track if logout label exist
await waitFor(() =>
expect(screen.queryByRole("userTitle")).toBeInTheDocument()
);
await waitFor(() =>
expect(screen.queryByTestId("logout")).toBeInTheDocument()
);
//TODO: click on logout button and mock localstorage maybe ?
const userLogout = screen.getByTestId("logout");
fireEvent.click(userLogout);
// should not be able to query the userTitle since it's logout
await waitFor(() =>
expect(screen.queryByRole("userTitle")).not.toBeInTheDocument()
);
});
I found out you can directly dispatch state through the store. which is really convenient.
Cause for me. What's tricky is I don't know how to write integration test across two components or pages
Right now if I can directly dispatch some state first. I can unit-test the function in single page or components to see if it does what I needed.
I am pretty new to testing, so if there's any better approach at this, please let me know!
Hope this can help someone who is struggling!!

How to display comments coming from Redux store on individual component

I have created a basic single page app, on initial page there is some dummy data and on click of each item I direct user to individual details page of that item. I wanted to implement comment and delete comment functionality which I successfully did but now when I comment or delete the comment it doesn't only happen at that individual page but in every other page too. Please see the sandbox example for better clarify.
https://codesandbox.io/s/objective-feistel-g62g0?file=/src/components/ProductDetails.js
So once you add some comments in individual page, go back and then click to another products, apparently you will see that the comments you've done in other pages are also available there. What do you think causing this problem ?
The same state being reused by all the different pages.
Try to load dynamically load reducers for each page/router differently to use distinct state values.
You can start from here
Redux modules and code splitting
I found my own logical solution. You probably might find a better solution but this works pretty well too. I thought of passing another property in the object with the params I get from url and then filter the comments by their url params. So that I could do filtering based on the url parameters and display the comments only made on that specific page.
So ProductDetails.js page should be looking like this:
import React, { useState, useEffect } from 'react';
import { Input, Button } from 'semantic-ui-react'
import { connect } from 'react-redux';
const ProductDetails = (props) => {
const [commentObject, setCommentObject] = useState({
text: "",
date: "",
id: ""
});
const clickHandler = () => {
if (!commentObject.text.trim()) {
return
}
props.addNewComment(commentObject)
setCommentObject({
...commentObject,
text: ""
})
console.log(commentObject.id);
}
useEffect(() => {
}, []);
return (
<div>
{props.posts ? props.posts.text : null}
{props.comments.filter(comment => {
return comment.postId === props.match.params.slug
}).map(({ text, id }) => {
return (<div key={id}>
<p>{text}</p>
<Button onClick={() => props.deleteComment(id)} >Delete comment</Button></div>)
})}
<Input value={commentObject.text}
onChange={comment => setCommentObject({ text: comment.target.value, date: new Date(), id: Date.now(), postId: props.match.params.slug })}
/>
<Button onClick={clickHandler} >Add comment</Button>
</div>
);
}
const mapStateToProps = (state, ownProps) => {
let slug = ownProps.match.params.slug;
return {
...state,
posts: state.posts.find(post => post.slug === slug),
}
}
const mapDispatchToProps = (dispatch) => {
return {
addNewComment: (object) => { dispatch({ type: "ADD_COMMENT", payload: { comment: { text: object.text, date: object.date, id: object.id, postId: object.postId } } }) },
deleteComment: (id) => { dispatch({ type: "DELETE_COMMENT", id: id }) }
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ProductDetails);

Why only one button?

I make a request to my local server using fetch() method. The server returns this response:
{
// total quantity elements in all page
"total":7,
// quantity elements in one page
"perPage":3,
// current page
"page":1,
// quantity page
"lastPage":3,
// it category list. I display my category list on page.
"data":[
{"id":1,"title":"animals","created_at":"/...","updated_at":"/..."},
{"id":2,"title":"space","created_at":"/...","updated_at":"/..."},
{"id":3,"title":"sport","created_at":"/...","updated_at":"/..."}
]
}
Also in my local server, I have ability to use query parameters page or limit which I insert in URL:
page - using this param I can implement pagination
limit - using this param I can implement choose quantity element
I have two tasks:
Make pagination (DONE)
Make ability to choose quantity element on page using three buttons (quantity three elements, quantity four elements, quantity five elements)
First task I've already done, however the second task is where I have a problem.
Instead of three buttons, I have only one button. When I click this button in my page it displays two elements. I also need to show the other 2 buttons which will display three and four elements respectively when clicked.
See screenshot:
What to fix in the code?
Maybe I wrote something wrong in the return?
I comment code line which implement choose quantity element
Home.js:
const Home = () => {
const [value, setValue] = useState({
listCategory: [],
currentPage: 1,
buttonsPagination: 0,
quantityElementPage: 3, // this line
buttonsQuantityElementPage: 3 // this line
});
useEffect(() => {
async function fetchData(currentPage, quantityElementPage) { // this line
try {
const res = await apiCategory('api/categories', {
method: 'GET',
}, currentPage, quantityElementPage ); // this line
console.log(res);
setValue({
listCategory: res.data,
currentPage: res.page,
buttonsPagination: Math.ceil(res.total / res.perPage),
quantityElementPage: res.perPage, // this line
});
} catch (e) {
console.error(e);
}
}
fetchData(value.currentPage, value.quantityElementPage); // this line
}, [value.currentPage, value.quantityElementPage]); // this line
const changePage = (argPage) => {
setValue((prev) => ({
...prev,
currentPage: argPage,
}));
};
const changeQuantityElementPage = (argElement) => { // this method
setValue((prev) => ({
...prev,
quantityElementPage: argElement,
}));
};
return (
<div>
<Table dataAttribute={value.listCategory} />
{[...Array(value.buttonsPagination)].map((item, index) => (
<button key={'listCategory' + index}
onClick={() => changePage(index + 1)}>{index + 1}
</button>
))}
//here I display button who choose quantity element:
{[...Array(value.buttonsQuantityElementPage)].map((item, index) => (
<button onClick={() => changeQuantityElementPage(index+2)}>quantity element - {index+2}
</button>
))}
</div>
);
};
apiCategory.js:
export const apiCategory = async (url, args, valuePage, valueElement) => { //add valueElement in argument
const getToken = localStorage.getItem('myToken');
const response = await fetch(`${apiUrl}${url}?page=${valuePage}&limit=${valueElement}`, { //add valueElement in param limit
...args,
headers: {
"Content-type": "application/json; charset=UTF-8 ",
"Authorization": `Bearer ${getToken}`,
"Accept": 'application/json',
...args.headers,
},
});
return response.json();
}
This is happening because your value.buttonsQuantityElementPage is undefined. When you call setValue in useEffect, you did not include the previous values. buttonsQuantityElementPage no longer exists on value.
Also, if the Array constructor Array() receives an undefined value, it will return an array with a single undefined value. i.e. [...Array(undefinedValue)] will yield [undefined]
There are two options for a fix.
Option 1. Update the setValue in your useEffect to include all previous values and then override the properties with new values.
setValue(prev => ({
...prev,
listCategory: res.data,
currentPage: res.page,
buttonsPagination: Math.ceil(res.total / res.perPage),
quantityElementPage: res.perPage,
}));
Option 2. Split out the buttonsQuantityElementPage into its own variable. As far as I can see in your code, buttonsQuantityElementPage does not change. If that is the case then use
const buttonsQuantityElementPage = 3
and in the return section
[...Array(buttonsQuantityElementPage)].map((item, index) => (
<button onClick={() => changeQuantityElementPage(index + 2)}>quantity element - {index + 2}
</button>
))

Using Reactjs to show Json response data on each form submission

How can I use Reactjs list records response on each form submission.
I have searched for previous post on this on stackoverflow but most solution I found does not address my issue.
The code below works but only list one record or
replace already existing displayed data on each form submission.
Here is what I want to achieve.
If I submit form 4 times am supposed to have 4 records displayed
For Instance
uid filename
1 macofile
2 johnfile
3 lukefile
4 tonyfile
But what this code does is to replace already existing record on each form submission and
as a result, it only show just one records
Eg. on 4th form submission it shows only
4 tonyfile
In angularjs I use something like push function to actualize my goal as per code below
$scope.users.push(res.data[0]);
In reactjs if I try the code below
const users = users.push(res.data);
//const users = users.push(res.data[0]);
it will show error
Cannot read property 'push' of undefined
Here is the code
import React, { Component } from "react";
import axios, { post } from "axios";
class FilePage extends React.Component {
constructor(props) {
super(props);
this.state = {
value: "",
filename: "",
loading: false,
users: [],
error: null
};
this.handleChange = this.handleChange.bind(this);
}
_handleSubmit(e) {
e.preventDefault();
//send it as form data
const formData = new FormData();
formData.append("filename", this.state.filename);
//alert(this.state.filename);
this.setState({ loading: true }, () => {
axios
.post("http://localhost/apidb_react/up.php", formData)
.then(res => {
//const users = res.data;
//const users = users.push(res.data[0]);
const users = users.push(res.data);
this.setState({ users, loading: false });
/*
this.setState({
users: res.data,
loading: false
});
*/
})
.catch(err => {
console.log(err.message);
});
});
}
// handle form submission
handleChange(event) {
this.setState({ [event.target.name]: event.target.value });
}
render() {
const { loading, users, error } = this.state;
return (
<div>
<form onSubmit={e => this._handleSubmit(e)}>
<b>filename:</b>
<input
tyle="text"
className="form-control"
value={this.state.filename}
name="filename"
onChange={this.handleChange}
/>
<button
className="submitButton"
type="submit"
onClick={e => this._handleSubmit(e)}
>
submit
</button>
</form>
<ul>
{this.state.users.map((user, index) =>
<option key={user.uid} value='{ user.uid }' > { user.uid } { user.filename}</option>
)}
</ul>
</div>
);
}
}
because the local user is not defined as an array to have .push() function.
const users=this.state.users;
users.push(res.data) then you can replace it with the users in the state.
what works for me is the concat functions as per code below
const users=this.state.users.concat(res.data);
// const users=this.state.users.push(res.data);// does not work.
Consequently, push() does not work because it returns the length of the extended array, instead of the array itself.
Thanks

Resources