I am trying to show a toast notification when a service worker updates but the toast notification shows up two times. It seems that its because of the useEffect hook but am not being able to understand how to fix the problem.
In App.js
import React, { Fragment, useState, useEffect } from "react";
import * as serviceWorker from "../serviceWorker";
import { useSnackbar } from "notistack";
const App = () => {
const [newVersionAvailable, setNewVersionAvailable] = useState(false);
const [waitingWorker, setWaitingWorker] = useState({});
const { enqueueSnackbar } = useSnackbar();
const onServiceWorkerUpdate = (registration) => {
setNewVersionAvailable(true);
setWaitingWorker(registration && registration.waiting);
}
const updateServiceWorker = () => {
if(waitingWorker) {
waitingWorker.addEventListener("statechange", event => {
if(event.target.state === "activated") {
setNewVersionAvailable(false);
window.location.reload();
}
})
waitingWorker.postMessage({ type: "SKIP_WAITING" });
}
}
const updateAction = () => {
return (
<Fragment>
<Button
className="snackbar-button"
size="small"
onClick={ updateServiceWorker }
>
{ "Update" }
</Button>
</Fragment>
)
}
useEffect(() => {
serviceWorker.register({ onUpdate: onServiceWorkerUpdate });
if(newVersionAvailable) {
enqueueSnackbar("New Update Available!", {
persist: true,
anchorOrigin: {
vertical: "bottom",
horizontal: "center",
},
action: updateAction,
})
}
})
return (...);
Add newVersionAvailable to the effect's dependency array, then set back to false once the toast it fired off so it isn't triggered again.
useEffect(() => {
serviceWorker.register({ onUpdate: onServiceWorkerUpdate });
if(newVersionAvailable) {
enqueueSnackbar("New Update Available!", {
persist: true,
anchorOrigin: {
vertical: "bottom",
horizontal: "center",
},
action: updateAction,
});
setNewVersionAvailable(false);
}
}, [newVersionAvailable])
Hard to know from the code you've posted but what (if any) props do you have ? As it stands your useEffect will respond to every prop change. Perhaps you need to add a dependency array and only have useEffect trigger on certain prop changes.
useEffect(() => {
serviceWorker.register({ onUpdate: onServiceWorkerUpdate });
if(newVersionAvailable) {
enqueueSnackbar("New Update Available!", {
persist: true,
anchorOrigin: {
vertical: "bottom",
horizontal: "center",
},
action: updateAction,
})
}
}, [prop_whose_change_you_want_to_trigger_this_useEffect])
Related
I'm using a npm package called ReactDataGrid which has SelectEditor module which renders a combo box. In the editorProps, I am able to set a function which needs to be called on onChange event. This function setClientonChange needs to call another function which is nested inside a function component? How can I call it?
import React, {useState } from 'react';
import ReactDataGrid from '#inovua/reactdatagrid-community';
const columns = [
...
{ name: 'currency_id', groupBy: false, defaultFlex: 1, maxWidth: 150, textAlign: 'center', header: 'Currency', editor: SelectEditor, editable:true,
editorProps: {
dataSource: ['Dollar', 'Euro', 'Pound', 'INR'].map((element) => ({
id: element,
label: element
})),
setClientonChange(){
//have to call setCurrencyValue() here
}
}
}
];
const RoomDeposit = () => {
const [gridRef, setGridRef] = useState(null);
const setCurrencyValue = () => {
gridRef.current.setItemPropertyAt(2, 'amount', '20')
}
return (
<ReactDataGrid
onReady={setGridRef}
columns={columns}
dataSource={dataSource}
/>
);
}
export default () => <RoomDeposit />
import React, { useState } from "react";
import ReactDataGrid from "#inovua/reactdatagrid-community";
const columns = (setCurrencyValue) => [
...{
name: "currency_id",
groupBy: false,
defaultFlex: 1,
maxWidth: 150,
textAlign: "center",
header: "Currency",
editor: SelectEditor,
editable: true,
editorProps: {
dataSource: ["Dollar", "Euro", "Pound", "INR"].map((element) => ({
id: element,
label: element,
})),
setClientonChange() {
//have to call setCurrencyValue() here
setCurrencyValue();
},
},
},
];
const RoomDeposit = () => {
const [gridRef, setGridRef] = useState(null);
const setCurrencyValue = () => {
gridRef.current.setItemPropertyAt(2, "amount", "20");
};
return (
<ReactDataGrid
onReady={setGridRef}
columns={columns(setCurrencyValue)}
dataSource={dataSource}
/>
);
};
export default () => <RoomDeposit />;
Really I am not able to understand the question self.
we can execute global function from inside of function.
Function should be declarer first then execute.
But as per my understanding the answer can be look like this.
import React, {useState } from 'react';
import ReactDataGrid from '#inovua/reactdatagrid-community';
const RoomDeposit = () => {
const [gridRef, setGridRef] = useState(null);
const setCurrencyValue = () => {
gridRef.current.setItemPropertyAt(2, 'amount', '20')
}
function onDropdownChange(){
setCurrencyValue(); //You can execute from here
}
return (
<ReactDataGrid
onReady={setGridRef}
columns={columns}
dataSource={dataSource}
/>
);
}
export default () => <RoomDeposit />
Here is what I have for now:
import {
Alert,
Animated,
Easing,
Linking,
StyleSheet,
Text,
View,
} from "react-native";
import React, { useEffect, useState } from "react";
import * as Location from "expo-location";
import * as geolib from "geolib";
import { COLORS } from "../../assets/Colors/Colors";
export default function DateFinder() {
const [hasForegroundPermissions, setHasForegroundPermissions] =
useState(null);
const [userLocation, setUserLocation] = useState(null);
const [userHeading, setUserHeading] = useState(null);
const [angle, setAngle] = useState(0);
useEffect(() => {
const AccessLocation = async () => {
function appSettings() {
console.warn("Open settigs pressed");
if (Platform.OS === "ios") {
Linking.openURL("app-settings:");
} else RNAndroidOpenSettings.appDetailsSettings();
}
const appSettingsALert = () => {
Alert.alert(
"Allow Wassupp to Use your Location",
"Open your app settings to allow Wassupp to access your current position. Without it, you won't be able to use the love compass",
[
{
text: "Cancel",
onPress: () => console.warn("Cancel pressed"),
},
{ text: "Open settings", onPress: appSettings },
]
);
};
const foregroundPermissions =
await Location.requestForegroundPermissionsAsync();
if (
foregroundPermissions.canAskAgain == false ||
foregroundPermissions.status == "denied"
) {
appSettingsALert();
}
setHasForegroundPermissions(foregroundPermissions.status === "granted");
if (foregroundPermissions.status == "granted") {
const location = await Location.watchPositionAsync(
{
accuracy: Location.Accuracy.BestForNavigation,
activityType: Location.ActivityType.Fitness,
distanceInterval: 0,
},
(location) => {
setUserLocation(location);
}
);
const heading = await Location.watchHeadingAsync((heading) => {
setUserHeading(heading.trueHeading);
});
}
};
AccessLocation().catch(console.error);
}, []);
useEffect(() => {
if (userLocation != null) {
setAngle(getBearing() - userHeading);
rotateImage(angle);
}
}, [userLocation]);
const textPosition = JSON.stringify(userLocation);
const getBearing = () => {
const bearing = geolib.getGreatCircleBearing(
{
latitude: userLocation.coords.latitude,
longitude: userLocation.coords.longitude,
},
{
latitude: 45.47200370608976,
longitude: -73.86246549592089,
}
);
return bearing;
};
const rotation = new Animated.Value(0);
console.warn(angle);
const rotateImage = (angle) => {
Animated.timing(rotation, {
toValue: angle,
duration: 1000,
easing: Easing.bounce,
useNativeDriver: true,
}).start();
};
//console.warn(rotation);
return (
<View style={styles.background}>
<Text>{textPosition}</Text>
<Animated.Image
source={require("../../assets/Compass/Arrow_up.png")}
style={[styles.image, { transform: [{ rotate: `${angle}deg` }] }]}
/>
</View>
);
}
const styles = StyleSheet.create({
background: {
backgroundColor: COLORS.background_Pale,
flex: 1,
// justifyContent: "flex-start",
//alignItems: "center",
},
image: {
flex: 1,
// height: null,
// width: null,
//alignItems: "center",
},
scrollView: {
backgroundColor: COLORS.background_Pale,
},
});
I think that the math I'm doing must be wrong because the arrow is pointing random directions spinning like crazy and not going to the coordinate I gave it. Also, I can't seem to use the rotateImage function in a way that rotation would be animated and i'd be able to use it to animate the image/compass. If anyone could help me out i'd really appreciate it I've been stuck on this for literally weeks.
I'm testing a state machine using model-based testing using #xstate/test and #testing-library/react.
Basically, I'm testing this machine:
const itemDamagedMachine = createMachine({
initial: 'newModal',
context: {
productScan: '',
binScan: '',
},
states: {
newModal: {
initial: 'scanDamagedItem',
states: {
scanDamagedItem: {},
scanDamagedBin: {},
declareItemDamaged: {},
},
},
closed: {},
},
on: {
UPDATE_PRODUCT_SCAN: {
actions: assign({
productScan: 123456,
}),
},
VALIDATE: {
target: 'newModal.scanDamagedBin',
},
UNREADABLE: {
target: 'newModal.scanDamagedBin',
},
CANCEL: {
target: 'closed',
},
UPDATE_DAMAGED_BIN_SCAN: {
actions: assign({
binScan: 'PB_DAMAGED',
}),
},
},
});
I'm then configuring the model, and testing it using const testPlans = itemDamagedModel.getSimplePathPlans();.
Everything seems to run smoothly with about 200 passing tests, but I'm having a few issues:
For each of my test and each of my event, I'm getting a warning Missing config for event "VALIDATE". I don't understand what it's supposed to mean.
All of my tests are validated even if I make typos on purpose in my model event. Sometimes the number of tests is reduced, but I would have hoped to see a few warnings when the model doesn't find a particular input or button.
The tests are all passing, even if I'm passing an empty div as my xstate/test rendered component.
I do not get the idea, but I have tested a component as follow:
First I have my machine:
import { createMachine, sendParent } from 'xstate';
export const machineDefinition = {
id: 'checkbox',
initial: 'unchecked',
states: {
unchecked: {
on: {
TOGGLE: [
{
actions: [ 'sendParent' ],
target: 'checked',
},
],
},
},
checked: {
on: {
TOGGLE: [
{
actions: [ 'sendParent' ],
target: 'unchecked',
},
],
},
},
},
};
const machineOptions = {
actions: {
sendParent: sendParent((context, event) => event.data),
},
};
export default createMachine(machineDefinition, machineOptions);
Second, I have extended the render method of testing-library
import React from 'react'
import HelmetProvider from 'react-navi-helmet-async'
import SpinnerProvider from '#atoms/GlobalSpinner'
import AlertProvider from '#molecules/GlobalAlert'
import InternationalizationProvider from '#internationalization/InternationalizationProvider'
import { render as originalRender } from '#testing-library/react'
const render = (ui, { locale = 'es', ...renderOptions } = {}) => {
const Wrapper = ({ children }) => {
return (
<InternationalizationProvider>
<AlertProvider>
<SpinnerProvider>
<HelmetProvider>
{children}
</HelmetProvider>
</SpinnerProvider>
</AlertProvider>
</InternationalizationProvider>
)
}
return originalRender(ui, { wrapper: Wrapper, ...renderOptions })
}
export * from '#testing-library/react'
export { render }
Finally, I have created the test
import React from 'react';
import { produce } from 'immer';
import { machineDefinition } from '#stateMachines/atoms/checkbox';
import { createMachine } from 'xstate';
import { createModel } from '#xstate/test';
import { render, cleanup, fireEvent } from '#root/jest.utils';
import Checkbox from '#atoms/Checkbox';
const getMachineDefinitionWithTests = () => produce(machineDefinition, (draft) => {
draft.states.unchecked.meta = {
test: ({ getByTestId }) => {
expect(getByTestId('checkbox-child-3')).toHaveClass('w-8 h-4 rounded-md duration-500 bg-dark-300 dark:bg-accent-100');
},
};
draft.states.checked.meta = {
test: ({ getByTestId }) => {
expect(getByTestId('checkbox-child-3')).toHaveClass('w-8 h-4 rounded-md duration-500 bg-dark-300 dark:bg-accent-100');
expect(getByTestId('checkbox-child-3.1')).toHaveClass('bg-light-100 w-4 h-4 rounded-full duration-500 dark:transform dark:translate-x-full');
},
};
});
const getEvents = () => ({
TOGGLE: {
exec: ({ getByTestId }) => {
fireEvent.click(getByTestId('checkbox-container'));
},
cases: [ {} ],
},
});
describe('checkbox', () => {
const machine = createMachine(getMachineDefinitionWithTests(), {
actions: {
sendParent: () => {},
},
});
const machineModel = createModel(machine)
.withEvents(getEvents());
const testPlans = machineModel.getSimplePathPlans();
testPlans.forEach((plan) => {
describe(plan.description, () => {
afterEach(cleanup);
plan.paths.forEach((path) => {
it(path.description, () => {
const rendered = render(
<Checkbox
test
label='main.txt1'
data={{}}
machine={machine}
/>,
{ locale: 'en' },
);
return path.test(rendered);
});
});
});
});
describe('coverage', () => {
it('should have full coverage', () => {
machineModel.testCoverage();
});
});
});
I have created a react boilerplate which contains XState, there you can find the previous test
I have an app with React front and Spring backend. I use Axios to fetch from the back. I have 2 class components with tables and I can access them via a menu component (in componentDidMount and componentDidUpdate only). I use all the possible precautions against infinite loops (loaded state and isMounted with a custom name). It works in the first component which I access after logging in. However, the second component (which is logically the same as the first, just has another entity to fetch) keeps requesting with axios until i go there (i see it in the network tab of my browser). Why can it be? it is definitely not mounted and console.logs don't work from there but while I'm on first it keeps requesting on and on (and it doesn't receive anything I guess, it is 0 bytes at this time)
import React, { Component } from 'react'
import {Link} from 'react-router-dom';
import axios from 'axios'
import "react-table/react-table.css";
import ReactTable from 'react-table';
import {Button, ButtonToolbar} from 'react-bootstrap';
import { LinkContainer } from "react-router-bootstrap";
import AddCalculationsModal from './AddCalculationsModal';
import UpdateCalculationsModal from './UpdateCalculationsModal';
import Cluster from './Cluster';
import Select from 'react-select/src/Select';
export default class Calculations extends Component {
isCMounted = false;
constructor(props) {
super(props)
this.state = {
items: [],
selected: null,
addModalShow: false,
updateModalShow: false,
updateId: null,
buttonOn: false,
page: 0,
elements: 0,
loaded: false
}
}
componentDidMount() {
this.isCMounted = true;
if(!this.state.loaded){
this.load();
}
};
componentDidUpdate() {
if(!this.state.loaded){
this.load();
}
};
componentWillUnmount(){
this.isCMounted = false;
}
increasePage = () => {
this.setState({
page: this.state.page + 1
})
}
decreasePage = () => {
this.setState({
page: this.state.page - 1
})
}
load = async () => {
await axios.get(`calculations?page=${this.state.page}&elements=${this.state.elements}`)
.then(res => {
if (this.isCMounted && this.state.items.id === res.data.id){
this.setState({items: res.data})
}
});
if(this.state.selected != null && this.isCMounted) {
this.setState({buttonOn: true})
}
this.setState({loaded: true})
}
setId = (id) => {
const idValue = this.state.items[id].id;
if (this.isCMounted)
this.setState({updateId: idValue});
}
deleteRow = (id) => {
const index = this.state.items.findIndex(item => {
return item.id === this.state.items[id].id})
const idValue = this.state.items[id].id
axios.delete(`calculations/${idValue}`).then(
res => {
this.load();
}
)
this.state.items.splice(index, 1)
this.load();
}
render() {
let addModalClose = () => this.setState({addModalShow: false});
let updateModalClose = () => this.setState({updateModalShow: false});
return (
<div>
<h3>Calculations</h3>
<ReactTable
columns={
[
{
Header: "ID",
accessor: "id"
},
{
Header: "Name",
accessor: "name"
},
{
Header: "Creation Date",
accessor: "dateCreate"
},
{
Header: "Update Date",
accessor: "dateUpdate"
},
{
Header: "User",
accessor: "userId"
}
]
}
data={this.state.items}
filterable
showPagination={false}
getTrProps={(state, rowInfo) => {
if (rowInfo && rowInfo.row) {
return {
onClick: (e) => {
this.setState({
selected: rowInfo.index
})
},
style: {
background: rowInfo.index === this.state.selected ? '#00afec' : 'white',
color: rowInfo.index === this.state.selected ? 'white' : 'black'
}
}
}else{
return {}
}
}}
>
</ReactTable>
<ButtonToolbar>
<Button variant="primary" onClick={() => {
this.decreasePage();
this.load();
}}>PREVIOUS PAGE</Button>
<Button variant="primary" onClick={() => {
this.increasePage();
this.load();
}}>NEXT PAGE</Button>
</ButtonToolbar>
<ButtonToolbar>
<Button variant="primary" onClick={() => this.setState({addModalShow: true})}>
Add Calculation
</Button>
<Button variant="primary" onClick={() => {
this.setId(this.state.selected);
this.setState({updateModalShow: true})}} disabled={this.state.buttonOn ? false : true}>
Update Calculation
</Button>
<Button variant="danger" onClick={() => {
this.deleteRow(this.state.selected);
}}>DELETE</Button>
<Link to={`/calculations/${this.state.items[this.state.selected] && this.state.items[this.state.selected].id}`}>
<Button variant="warning" disabled={this.state.buttonOn ? false : true}>Cluster</Button>
</Link>
<AddCalculationsModal
show={this.state.addModalShow}
onHide={addModalClose}
calculation={this.state.items[this.state.selected]}
/>
<UpdateCalculationsModal
show={this.state.updateModalShow}
onHide={updateModalClose}
calculation={this.state.items[this.state.selected] && this.state.items[this.state.selected].id}
calcname={this.state.items[this.state.selected] && this.state.items[this.state.selected].name}
/>
</ButtonToolbar>
</div>
)
}
}
And
import React, { Component } from 'react'
import axios from 'axios'
import "react-table/react-table.css";
import ReactTable from 'react-table';
import {Button, ButtonToolbar} from 'react-bootstrap';
import AuthenticationService from '../service/AuthenticationService';
export default class Calculations extends Component {
isCMounted = false;
constructor(props) {
super(props)
this.state = {
items: [],
selected: null,
updateId: null,
loaded: false
}
}
componentDidMount() {
this.isCMounted = true;
if(!this.state.loaded) {
this.load();
}
};
componentDidUpdate() {
if(!this.state.loaded) {
this.load();
}
};
componentWillUnmount() {
this.isCMounted = false;
}
load = async () => {
if(this.isCMounted && !this.state.loaded) {
await axios.get('calculation-types')
.then(res => {
console.log(this.isCMounted)
if (this.isCMounted && this.state.items.id === res.data.id){
this.setState({items: res.data})
}
});
this.setState({loaded: true})
}
}
setId = (id) => {
const idValue = this.state.items[id].id;
if (this.isCMounted)
this.setState({updateId: idValue});
}
render() {
return (
<div>
<h3>Calculation Types</h3>
<ReactTable
columns={
[
{
Header: "ID",
accessor: "idType",
width: 100,
minWidth: 100,
maxWidth: 100
},
{
Header: "Name",
accessor: "name"
}
]
}
data={this.state.items}
filterable
showPagination={false}
getTrProps={(state, rowInfo) => {
if (rowInfo && rowInfo.row) {
return {
onClick: (e) => {
this.setState({
selected: rowInfo.index
})
},
style: {
background: rowInfo.index === this.state.selected ? '#00afec' : 'white',
color: rowInfo.index === this.state.selected ? 'white' : 'black'
}
}
}else{
return {}
}
}}
>
</ReactTable>
</div>
)
}
}
are my components. Menu is a normal link. after login i appear on the first with menu on top.
Have you tried moving this.setState({loaded: true}) into the axios response callback block? Since you're awaiting the fetch request, I wonder if the this.setState({items: res.data} that you have in the callback block is causing an infinite componentDidUpdate loop that causes load to be repeatedly called without ever having the chance to arrive at the this.setState({loaded: true}) in the final line of load.
load = async () => {
if(this.isCMounted && !this.state.loaded) {
await axios.get('calculation-types')
.then(res => {
console.log(this.isCMounted)
if (this.isCMounted && this.state.items.id === res.data.id){
this.setState({ items: res.data, loaded: true })
}
});
}
}
I'm calling an api and setting the state with the response.
I'm not able to call setState without seeing this error:
The error does not occur if setState happens outside the promise.
How do you set an API response to state without seeing this error?
Can't call setState (or forceUpdate) on an unmounted component
componentDidMount() {
const url = 'https://localhost:8000/api/items'
let items;
fetch(url, {mode: 'cors'})
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
items: result
});
},
(error) => {
this.setState({
isLoaded: true,
error
});
}
)
}
Entire component for reference:
import React, { Component } from 'react'
import PropTypes from 'prop-types';
import { withStyles } from '#material-ui/core/styles';
import GridList from '#material-ui/core/GridList';
import GridListTile from '#material-ui/core/GridListTile';
import GridListTileBar from '#material-ui/core/GridListTileBar';
import ListSubheader from '#material-ui/core/ListSubheader';
import IconButton from '#material-ui/core/IconButton';
import InfoIcon from '#material-ui/icons/Info';
const styles = theme => ({
root: {
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'space-around',
overflow: 'hidden',
backgroundColor: theme.palette.background.paper,
},
icon: {
color: 'rgba(255, 255, 255, 0.54)',
},
});
class ItemList extends Component {
constructor(props) {
super(props)
this.state = {
items: [],
isLoaded: false,
}
this.handleItemClick = this.handleItemClick.bind(this);
}
componentDidMount() {
const url = 'https://localhost:8000/api/items'
let items;
fetch(url, {mode: 'cors'})
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
items: result
});
},
(error) => {
this.setState({
isLoaded: true,
error
});
}
)
}
handleItemClick(id) {
}
handleRenderItems() {
const { classes } = this.props
const { items } = this.state;
return this.state.items.map((item, idx) => {
const id = item.id;
return (
<GridListTile onClick={() => this.handleItemClick(id)} key={idx}>
<img src={item.key_image} alt={item.title} />
<GridListTileBar
title={item.title}
subtitle={<span>${item.rent_price_day}/day</span>}
actionIcon={
<IconButton className={classes.icon}>
<InfoIcon />
</IconButton>
}
/>
</GridListTile>
)
})
}
render() {
const { classes } = this.props;
return (
<div className={classes.root}>
<GridList cols={3} cellHeight={220} className={classes.gridList}>
<GridListTile key="Subheader" cols={3} style={{ height: 'auto' }}>
<ListSubheader component="div">Popular Items</ListSubheader>
</GridListTile>
{this.handleRenderItems()}
</GridList>
</div>
);
}
}
ItemList.propTypes = {
classes: PropTypes.object.isRequired,
};
export default withStyles(styles)(ItemList);
Instead of handling this in the component - I think it may be simpler to handle the call to the API in an action creator, and use redux to store the data - then just send it to the component when it's available. Better to keep these details out of the view anyway.
Also, wouldn't typically use a style object in the view either - but it's boilerplate with material-ui.
Add below inside your component,
componentDidMount() {
this.isMounted = true;
}
componentWillUnmount() {
this.isMounted = false;
}
Then setState only if this.isMounted is true. This should solve your problem, but this is not a recommended pattern. Please refer https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html
Check the code of parent component which holds the <ItemList/>. If you are rendering <ItemList/> based on certain conditions, the problem might be in those conditions.
It simple. You just have to return the value of the second promise in a variable.
Like this :
let items;
let errorFetch;
fetch(url, {mode: 'cors'})
.then(res => res.json())
.then(
(result) => {
items = result;
},
(error) => {
errorFetch = error
}
);
this.setState(items ? { isLoaded : true, items } : { isLoaded : true, error: errorFetch });