So I get items from a DB and console.log them just before, so I know they exist and are in the format I want them to be. I also wrote a simple method to make sure rendering works. The code looks something like this (is simplified for nonrelevantbits)
const renderSomething = () => {
getFromDB().then(function(data){
if (data) {
console.log(data.something) //returns an array in console
return data.something.map(somet => {
console.log(' somet is ' + somet) //return somet with quotes: ""
return <p>{somet}</p>
})
} else {
console.log("No data!");
}
}
}
and then in the return:
return (
<div>
{renderSomething()}
</div>
)
And nothing appears on the screen. I made a test one to make sure it should:
const basic = () => {
return ['abc', 'bca'].map(num => <h1>{num}</h1>)
}
return (
<div>
{basic()}
</div>
)
The test one worked
Render function cannot be a promise. You should useEffect method to downlaod the data from api and then useState to set it for the component.
import React, { Fragment, useState, useEffect } from "react";
import ReactDOM from "react-dom";
function LiveVisitors() {
const [visitors, setVisitors] = useState([
{
ip: "",
countryCode: "",
city: "Warsaw",
state: "",
country: "Poland"
},
{
ip: "",
countryCode: "",
city: "Gdańsk",
state: "",
country: "Poland"
}
]);
const { ip, countryCode, city, state, country } = visitors;
useEffect(() => {
getUserData();
}, []);
const getUserData = () => {
//imitate fetch
setVisitors([
...visitors,
{
ip: "",
countryCode: "",
city: "Wrocław",
state: "",
country: "Poland"
}
]);
};
return (
<div>
{visitors.map((el) => (
<div>{el.city}</div>
))}
</div>
);
}
const wrapper = document.getElementById("container");
ReactDOM.render(<LiveVisitors />, wrapper);
Here is the interactive version where you can play with it.
https://codesandbox.io/s/react-playground-forked-hqswi?file=/index.js:0-947
Related
I am using Stripe API on my project and when the payment is successful I want to add the order placed to my collection. When I run the below code it adds the order multiple times and there is no specific pattern. It also adds multiple orders to the collection:
Success.js:
import React from "react";
import AddOrder from "../orders/AddOrder";
import { useNavigate, useParams } from "react-router-dom";
import { useMutation, useQuery } from "#apollo/client";
import queries from "../../queries";
import { Alert, Button } from "react-bootstrap";
function Success() {
const logo = require("../../assets/delivery-package.gif");
const navigate = useNavigate();
const { secret } = useParams();
const { loading, error, data } = useQuery(queries.GET_SESSION, { variables: { id: secret } });
const [deleteSession] = useMutation(queries.DELETE_SESSION);
if (loading) {
return <div>Loading...</div>;
} else if (error) {
navigate("/notfound");
} else if (data.session) {
deleteSession({
variables: {
id: secret,
},
});
return (
<div>
<AddOrder />
<Alert variant="success" style={{ fontSize: "25px" }}>
Order Placed Successfully
</Alert>
<img alt="order success" id="logo" src={logo} style={{ width: "70%", height: "70%", marginTop: "30px" }} />
<br />
<br />
<Button onClick={() => navigate("/")}>Home</Button>
</div>
);
} else {
navigate("/notfound");
}
}
export default Success;
AddOrder:
import { useMutation, useQuery } from "#apollo/client";
import { AuthContext } from "../../Firebase/Auth";
import queries from "../../queries";
import { useContext, useEffect } from "react";
import { reactLocalStorage } from "reactjs-localstorage";
let add = reactLocalStorage.getObject("addressDetails");
function AddOrder() {
const d = new Date();
let text = d.toString();
const [addOrder] = useMutation(queries.ADD_ORDER);
const [editUser] = useMutation(queries.EDIT_USER_CART);
const { currentUser } = useContext(AuthContext);
const { data } = useQuery(queries.GET_USER_BY_ID, {
fetchPolicy: "network-only",
variables: {
id: currentUser.uid,
},
});
const getUserOrders = useQuery(queries.GET_USER_ORDERS, {
fetchPolicy: "network-only",
variables: {
userId: currentUser.uid,
},
});
useEffect(() => {
if (data && getUserOrders.data && currentUser && data.getUser.cart.length > 0) {
let newCart = [];
let total = 0;
for (let i = 0; i < data.getUser.cart.length; i++) {
total += data.getUser.cart[i].price * data.getUser.cart[i].quantity;
newCart.push({
orderedQuantity: data.getUser.cart[i].quantity,
_id: data.getUser.cart[i]._id,
name: data.getUser.cart[i].name,
image: data.getUser.cart[i].image,
price: data.getUser.cart[i].price,
});
}
addOrder({
variables: {
userId: currentUser.uid,
userEmail: currentUser.email,
status: "ordered",
createdAt: text,
products: newCart,
total: total,
flag: getUserOrders.data.userOrders.length + 1,
zip: add.zip.val ? add.zip.val : add.zip,
state: add.state.val ? add.state.val : add.state,
city: add.city.val ? add.city.val : add.city,
apt: add.apt.val ? add.apt.val : add.apt,
addressStreet: add.addressStreet.val ? add.addressStreet.val : add.addressStreet,
},
});
editUser({
variables: {
id: currentUser.uid,
cart: [],
},
});
}
}, [addOrder, currentUser, data, editUser, getUserOrders, text]);
}
export default AddOrder;
There is only one return so don't know if the issue is with the success page or the Addorder function
Possible reason:
Your component must be being rendered by some parent component, for example, by calls to setState, and when the component is re-rendered the Success child component (which in turn does not return a JSX.Element) is also re-rendered
Cause:
Inside your AddOrder component there is a useEffect that serves to prevent re-renders when the passed parameters were not changed, and among them it has a "new Date().getTime()", I believe this is one of the main reasons why useEffect is always returning true, and requesting a re-render. Not only that, but also:
new Date().toString as parameter to useEffect
Passing functions (addOrder, editOrder) to useEffect params that are likely to always be new functions when reredendering.
currentUser is probably a new object every render, luckily you only need the userid, which in turn is a primitive type, and will always be the same regardless of re-rendering.
Solution:
There is no need to analyze if the addOrder or editOrder function has changed, as they are just functions, and do not add any specific "value", for example, if your intention is to rederize (to be able to add) only when the items change, you can just leave the getUserOrders.data and data (useQuery(queries.GET_USER_BY_ID)...)
New code:
import { useMutation, useQuery } from "#apollo/client";
import { AuthContext } from "../../Firebase/Auth";
import queries from "../../queries";
import { useContext, useEffect } from "react";
import { reactLocalStorage } from "reactjs-localstorage";
let add = reactLocalStorage.getObject("addressDetails");
function AddOrder() {
const d = new Date();
let text = d.toString();
const [addOrder] = useMutation(queries.ADD_ORDER);
const [editUser] = useMutation(queries.EDIT_USER_CART);
const { currentUser } = useContext(AuthContext);
const { data } = useQuery(queries.GET_USER_BY_ID, {
fetchPolicy: "network-only",
variables: {
id: currentUser.uid,
},
});
const getUserOrders = useQuery(queries.GET_USER_ORDERS, {
fetchPolicy: "network-only",
variables: {
userId: currentUser.uid,
},
});
useEffect(() => {
if (data && getUserOrders.data && currentUser && data.getUser.cart.length > 0) {
let newCart = [];
let total = 0;
for (let i = 0; i < data.getUser.cart.length; i++) {
total += data.getUser.cart[i].price * data.getUser.cart[i].quantity;
newCart.push({
orderedQuantity: data.getUser.cart[i].quantity,
_id: data.getUser.cart[i]._id,
name: data.getUser.cart[i].name,
image: data.getUser.cart[i].image,
price: data.getUser.cart[i].price,
});
}
addOrder({
variables: {
userId: currentUser.uid,
userEmail: currentUser.email,
status: "ordered",
createdAt: text,
products: newCart,
total: total,
flag: getUserOrders.data.userOrders.length + 1,
zip: add.zip.val ? add.zip.val : add.zip,
state: add.state.val ? add.state.val : add.state,
city: add.city.val ? add.city.val : add.city,
apt: add.apt.val ? add.apt.val : add.apt,
addressStreet: add.addressStreet.val ? add.addressStreet.val : add.addressStreet,
},
});
editUser({
variables: {
id: currentUser.uid,
cart: [],
},
});
}
}, [currentUser.uid, data, getUserOrders.data]);
}
export default AddOrder;
Please check if the code works, anything please comment so I can correct my answer (I haven't tested it yet, but I believe it will work).
Although it is not recommended, as the good practice recommends the use of memos, you can try in the last case (only to verify if this is in fact the problem) to use as follows:
[currentUser.uid, JSON.stringify(data), JSON.stringify(getUserOrders.data)]
In AddOrder component, you are not returning any JSX. You are actually sending order request in the body of the component ( during rendering, which is super bad since it is a side-effect).
Don't return addOrder, editUser calls. Place them inside of useEffect or event handlers depending on your business logic.
I faced with problem while migrating from Mobx 4 to Mobx 6.
I have a functional component but after updating Mobx it stopped working. Looks like store doesn't works. Component react on changes inside observable variable by reaction feature but changes aren't re-rendering. I made everything that was provided in migration guide but component's store doesn't working.
At some reason if I change functional component to class component everything starts working. But I really can't understand the reason why such happens and can't find any explanation of such behaviour.
Case looks like example bellow. Experimental decorators are enabled and any other stuff that was provided in Migration guide as well. So what is the reason of such behaviour and how can I implement correct logic in functional component?
interface User {
name: string;
age: number;
info: {
phone: string;
email: string;
};
}
const usersData: User[] = [
{
name: "Steve",
age: 29,
info: {
phone: "+79011054333",
email: "steve1991#gmail.com",
},
},
{
name: "George",
age: 34,
info: {
phone: "+79283030322",
email: "george_the_best_777#gmail.com",
},
},
{
name: "Roger",
age: 17,
info: {
phone: "+79034451202",
email: "rodge_pirat_yohoho#gmail.com",
},
},
{
name: "Maria",
age: 22,
info: {
phone: "+79020114849",
email: "bunnyrabbit013#gmail.com",
},
},
];
const getUsers = () => {
return new Promise<User[]>((resolve) => {
setTimeout(() => {
resolve(usersData);
}, 2000);
});
};
class Store {
#observable users: User[] = [];
constructor() {
makeObservable(this);
}
async init() {
const users = await getUsers();
this.setUsers(users);
}
#action setUsers(users: User[]) {
this.users = users;
}
#action increaseUserAge(userIndex: number) {
const users = this.users.map((u, k) => {
if (k === userIndex) {
u.age += 1;
}
return u;
});
this.setUsers(users);
}
#computed get usersCount(): number {
return this.users.length;
}
}
const store = new Store();
const UserList = observer(() => {
React.useEffect(() => {
store.init();
}, []);
const addOneUser = () => {
const user = {
name: "Jesica",
age: 18,
info: {
phone: "+79886492224",
email: "jes3331#gmail.com",
},
};
store.setUsers([...store.users, user]);
};
return (
<div className="App">
<h4>Users: {store.usersCount}</h4>
{store.users.length ? (
<>
<ul>
{store.users.map((user, key) => (
<li key={key}>
Name: {user.name}, Age: {user.age}, Info:
<div>
Phone: {user.info.phone}, Email: {user.info.email}
</div>
<button onClick={() => store.increaseUserAge(key)}>
Increase Age
</button>
</li>
))}
</ul>
<button onClick={addOneUser} disabled={store.usersCount >= 5}>
Add one user
</button>
</>
) : (
<p>Fetching users...</p>
)}
</div>
);
});
function App() {
return <UserList />;
}
export default App;
I've made Codesandbox example with your code (although removed types), it works fine.
Check tsconfig.json there, maybe you forgot to enable some of the options?
Or check what versions of mobx and mobx-react are you using?
And just a small nitpick on how you use your increaseUserAge action, it can be as simple as that:
#action increaseUserAge(user) {
user.age += 1;
}
And in the jsx you just pass the whole user there:
<button onClick={() => store.increaseUserAge(user)}>
Increase Age
</button>
I am using a weather app API to create a weather application.
I was able to reach the point that I could've print the weather if I hard coded the country in the link given.
My aim was to :
1 - Print the value of the city when I write it in the input and take the value and insert it in the link.
2 - unfortunately I couldn't do that [the point i reached is that I could get the input, but it won't accept it in the link]
Hint: I am a React beginner and trying my best to make my code work and using the DRY principle.
I appreciate your time and could use your help here.
Thanks in advance.
//Importing
import React, { Component } from "react";
import axios from "axios";
//syntax
let inputValue = "";
console.log(" input value before updated", inputValue);
export default class main extends Component {
state = {
showList: false,
data: "",
temp: "",
feels_like: "",
humidity: "",
pressure: "",
name: "",
country: "",
description: "",
icon: "",
weather: "",
};
//Component Did mount
handleInput = (event) => {
this.setState({
data: event.target.value,
});
};
submitted = () => {
inputValue = this.state.data;
const show = this.state.showList;
this.setState({ showList: !show });
};
componentDidMount() {
axios
.get(
`http://api.openweathermap.org/data/2.5/weather?q=${inputValue},uk&APPID=xxxxxxxxxxxxxxxxx`
)
.then((response) => {
this.setState({
temp: response.data.main.temp,
feels_like: response.data.main.feels_like,
humidity: response.data.main.humidity,
pressure: response.data.main.pressure,
name: response.data.name,
country: response.data.sys.country,
description: response.data.weather[0].description,
icon: response.data.weather[0].icon,
weather: response.data.weather[0].main,
});
})
.catch((error) => console.log(error));
}
render() {
let feels_like = this.state.feels_like;
let temp = this.state.temp;
let humidity = this.state.humidity;
let pressure = this.state.pressure;
let name = this.state.name;
let country = this.state.country;
let description = this.state.description;
let icon = this.state.icon;
let weather = this.state.weather;
let list = null;
if (this.state.showList) {
list = (
<div>
<h1>{this.state.data}</h1>
<h1>{feels_like}</h1>
<h1>{temp}</h1>
<h1>{humidity}</h1>
<h1>{pressure}</h1>
<h1>{name}</h1>
<h1>{country}</h1>
<h1>{description}</h1>
<img
alt=""
src={`http://openweathermap.org/img/wn/${icon}.png`}
></img>
<h1>{weather}</h1>
</div>
);
}
return (
<div>
<input
onChange={this.handleInput}
typw="text"
placeholder="Enter a country name"
/>
<button onClick={this.submitted}>Search</button>
{list}
</div>
);
}
}
Extra Info:
.get(
`http://api.openweathermap.org/data/2.5/weather?q=${inputValue},uk&APPID=xxxxxxxxxxxxxxxxx`
)
This is the part where I got 404 since it is not accepting the ${inputValue}
Make the inputValue part of the state. There is no reason to keep it as a global variable outside of the component like that.
I've got a fairly simple example of a component (Hello.js) that renders three components, each with a different id (Speaker.js). I have a clickFunction that I pass back from the Speaker.js. I would think that using React.memo and React.useCallback would stop all three from re-rendering when only one changes, but sadly, you can see from the console.log in Speaker.js, clicking any of the three buttons causes all three to render.
Here is the problem example on stackblitz:
https://stackblitz.com/edit/react-dmclqm
Hello.js
import React, { useCallback, useState } from "react";
import Speaker from "./Speaker";
export default () => {
const speakersArray = [
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true },
];
const [speakers, setSpeakers] = useState(speakersArray);
const clickFunction = useCallback((speakerIdClicked) => {
var speakersArrayUpdated = speakers.map((rec) => {
if (rec.id === speakerIdClicked) {
rec.favorite = !rec.favorite;
}
return rec;
});
setSpeakers(speakersArrayUpdated);
},[speakers]);
return (
<div>
{speakers.map((rec) => {
return (
<Speaker
speaker={rec}
key={rec.id}
clickFunction={clickFunction}
></Speaker>
);
})}
</div>
);
};
Speaker.js
import React from "react";
export default React.memo(({ speaker, clickFunction }) => {
console.log(`speaker ${speaker.id} ${speaker.name} ${speaker.favorite}`);
return (
<button
onClick={() => {
clickFunction(speaker.id);
}}
>
{speaker.name} {speaker.id} {speaker.favorite === true ? "true" : "false"}
</button>
);
});
because when you fire clickFunction it update speakers wich cause the recreating of this functions, to solve this you need to remove speakers from clickFunction dependencies and accessing it from setState callback.
here the solution :
import React, { useCallback, useState,useEffect } from "react";
import Speaker from "./Speaker";
export default () => {
const [speakers, setSpeakers] = useState([
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true },
]);
const clickFunction = useCallback((speakerIdClicked) => {
setSpeakers(currentState=>currentState.map((rec) => {
if (rec.id === speakerIdClicked) {
rec.favorite = !rec.favorite;
return {...rec};
}
return rec
}));
},[]);
useEffect(()=>{
console.log("render")
})
return (
<div>
{speakers.map((rec) => {
return (
<Speaker
speaker={rec}
key={rec.id}
clickFunction={clickFunction}
></Speaker>
);
})}
</div>
);
};
and for speaker component:
import React from "react";
export default React.memo(({ speaker, clickFunction }) => {
return (
<button
onClick={() => {
clickFunction(speaker.id);
}}
>
{speaker.name} {speaker.id} {speaker.favorite === true ? "true" : "false"}
</button>
);
});
Upon further reflection, I think my answer may not be entirely correct: without the [speakers] dependency this won't work as intended.
Two things:
The [speakers] dependency passed to useCallback causes the function to get recreated every time speakers changes, and because the callback itself calls setSpeakers, it will get recreated on every render.
If you fix #1, the Speaker components won't re-render at all, because they're receiving the same speaker prop. The fact that speaker.favorite has changed doesn't trigger a re-render because speaker is still the same object. To fix this, have your click function return a copy of rec with favorite flipped instead of just toggling it in the existing object:
import React, { useCallback, useState } from "react";
import Speaker from "./Speaker";
export default () => {
const speakersArray = [
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true },
];
const [speakers, setSpeakers] = useState(speakersArray);
const clickFunction = useCallback((speakerIdClicked) => {
var speakersArrayUpdated = speakers.map((rec) => {
if (rec.id === speakerIdClicked) {
return { ...rec, favorite: !rec.favorite }; // <= return a copy of rec
}
return rec;
});
setSpeakers(speakersArrayUpdated);
}, []); // <= remove speakers dependency
return (
<div>
{speakers.map((rec) => {
return (
<Speaker
speaker={rec}
key={rec.id}
clickFunction={clickFunction}
></Speaker>
);
})}
</div>
);
};
snapshot file has created before componentDidMount() is being called. In my situation, I fetch data from server inside the componentDidMount(). Based on the results, I draw the table. But in my test case, it doesn't show those received mock results.
Test file
import React from 'react';
import renderer from 'react-test-renderer';
import { fakeRequestLibrary } from '../../../__mocks__/fakeRequestLibrary';
import ReportAsTableView from '../../../components/reports/common/ReportAsTableView';
const FAKE_RESPONSE = {
dataSets: [
{
metadata: {
columns: [
{
name: "username",
label: "username"
},
{
name: "date_created",
label: "date_created"
}
]
},
rows: [
{
date_created: "2010-04-26T13:25:00.000+0530",
username: "daemon"
},
{
date_created: "2017-06-08T21:37:18.000+0530",
username: "clerk"
},
{
date_created: "2017-07-08T21:37:18.000+0530",
username: "nurse"
},
{
date_created: "2017-07-08T21:37:19.000+0530",
username: "doctor"
},
{
date_created: "2017-07-08T21:37:18.000+0530",
username: "sysadmin"
}
]
}
]
};
describe('<ReportAsTableView /> ', () => {
it('renders correctly with success data received from server', () => {
const params = {
"startDate": "2017-05-05",
"endDate": "2017-10-05"
};
var rendered = renderer.create(
<ReportAsTableView reportUUID="e451ae04-4881-11e7-a919-92ebcb67fe33"
reportParameters={params}
fetchData={fakeRequestLibrary('openmrs-fake-server.org', {}, true, FAKE_RESPONSE)} />
);
expect(rendered.toJSON()).toMatchSnapshot();
});
});
Targeted component class
import React, { Component } from 'react';
import { ApiHelper } from '../../../helpers/apiHelper';
import * as ReportConstants from '../../../helpers/ReportConstants';
import ReactDataGrid from 'react-data-grid';
import DataNotFound from './DataNotFound';
import moment from 'moment';
import './ReportAsTableView.css';
class ReportAsTableView extends Component {
constructor(props) {
super();
this.state = {
report: {
definition: {
name: ''
}
},
reportColumnNames: Array(),
reportRowData: Array()
};
this.resolveResponse = this.resolveResponse.bind(this);
this.rowGetter = this.rowGetter.bind(this);
this.init = this.init.bind(this);
}
componentDidMount() {
this.init(this.props.reportParameters);
}
componentWillReceiveProps(nextProps) {
this.init(nextProps.reportParameters);
}
init(params) {
if(this.props.fetchData != null){
//Test Path
this.props.fetchData
.then((response) => {
console.log('>>>>>'+JSON.stringify(response.body));
this.resolveResponse(response.body);
});
}else{
new ApiHelper().post(ReportConstants.REPORT_REQUEST + this.props.reportUUID, params)
.then((response) => {
this.resolveResponse(response);
});
}
}
resolveResponse(data) {
this.setState({ report: data });
this.setState({ reportColumnNames: data.dataSets[0].metadata.columns });
this.setState({ reportRowData: data.dataSets[0].rows });
}
// ... there are some other methods as well
render() {
return (
<div style={{ border: '1px solid black' }}>
{this.getColumns().length > 0 ? (
<ReactDataGrid
columns={this.getColumns()}
rowGetter={this.rowGetter}
rowsCount={this.state.reportRowData.length} />
) : (
<DataNotFound componentName="Report Table"/>
)}
</div>
);
}
}
export default ReportAsTableView;
Snapshot file
// Jest Snapshot v1,
exports[`<ReportAsTableView /> renders correctly with success data received from server 1`] = `
<div
style={
Object {
"border": "1px solid black",
}
}
>
<div
className="NotFoundWrapper"
>
<div
className="attentionSign"
>
<img
src="./warning.png"
width="300"
/>
</div>
<div>
No Data found
<span>
for
Report Table
</span>
</div>
</div>
</div>
`;
Update:
fakeRequestLibrary
import Response from 'http-response-object';
export const fakeRequestLibrary = (requestUrl, requestOptions, shouldPass = true, responseData = null) => {
return new Promise((resolve, reject) => {
if (shouldPass) {
resolve(new Response(200, {}, responseData || { message: `You called ${requestUrl}` }, requestUrl));
} else {
reject(new Response(404, {}, responseData || { message: `The page at ${requestUrl} was not found` }, requestUrl));
}
});
};
Instead of passing an http end point what you can do for fix your problem is changing your init method and passing the data if no data are passed fetch them. Like this
init(params) {
if(this.props.fetchData != null){
this.resolveResponse(this.props.fetchData);
}else{
new ApiHelper().post(ReportConstants.REPORT_REQUEST + this.props.reportUUID, params)
.then((response) => {
this.resolveResponse(response);
});
}
}
Then in your test you will have
var rendered = renderer.create(
<ReportAsTableView reportUUID="e451ae04-4881-11e7-a919-92ebcb67fe33"
reportParameters={params}
fetchData={FAKE_RESPONSE} />
);
expect(rendered.toJSON()).toMatchSnapshot();
This solution works for my own project. It might also work for this question as well, but I haven't tested it. Add an await wait(); statement to wait for the async function in componentDidMount to complete.
const wait = async () => 'foo'; // a dummy function to simulate waiting
describe('<ReportAsTableView /> ', async () => {
it('renders correctly with success data received from server', async () => {
const params = {
startDate: '2017-05-05',
endDate: '2017-10-05',
};
var rendered = renderer.create(
<ReportAsTableView
reportUUID="e451ae04-4881-11e7-a919-92ebcb67fe33"
reportParameters={params}
fetchData={fakeRequestLibrary(
'openmrs-fake-server.org',
{},
true,
FAKE_RESPONSE,
)}
/>,
);
await wait(); // wait for <ReportAsTableView> to finish async data fetch in componentDidMount()
expect(rendered.toJSON()).toMatchSnapshot(); // shall render the component AFTER componentDidMount() is called
});
});