How to render multiple notifications with help of notistack in react? - javascript

I'm trying to use React's 'notistack' module to display several notifications as a stack. However, it appears that I am making a mistake, as Whenever I receive a warning:
react_devtools_backend.js:3973 Warning: Cannot update during an existing state transition (such as within render)
I'm using the state here to set the Response messages(they are coming from api), which are an array of notifications. Once the alerts have been rendered, I should also make the state of the Response messages an empty array.
renderPage.js
import React, { useState, useContext } from "react";
import axios from "axios";
import Button from "#mui/material/Button";
import Typography from "#mui/material/Typography";
import { SnackbarProvider } from "notistack";
import Zoom from "#material-ui/core/Slide";
import Notification from "../../components/Notification";
import { handleResponse } from "../../services/responseHandler";
import { SelectedRunnerContext } from "./RunnersPage";
export function RunnersForm() {
const { selected } = useContext(SelectedRunnerContext);
const [responseMessages, setResponseMessages] = useState([]);
const notistackRef = React.createRef();
const onClickDismiss = (key) => () => {
notistackRef.current.closeSnackbar(key);
};
const MakePostRequest = (event) => {
event.preventDefault();
setResponseMessages([]);
const data = {
selected_runners: selected,
};
const requestOptions = {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: data,
};
axios
.post("/messages", requestOptions)
.then((response) => {
const result = handleResponse(response.data);
setResponseMessages(result);
})
.catch((error) => {
console.log(error);
});
};
return (
<>
<Button onClick={MakePostRequest}>Post</Button>
{!!responseMessages.length && (
<SnackbarProvider
maxSnack={4}
ref={notistackRef}
dense
preventDuplicate
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
TransitionComponent={Zoom}
sx={{
width: 700,
}}
style={{
fontSize: 15,
fontWeight: 700,
}}
action={(key) => (
<Button onClick={onClickDismiss(key)}>
<Typography
variant="subtitle2"
style={{
fontWeight: 600,
fontSize: 16,
color: "#00579b",
}}
>
Dismiss
</Typography>
</Button>
)}
>
<Notification messages={responseMessages} />
</SnackbarProvider>
)}
</>
);
}
Notification.js
import React, { Fragment } from "react";
import { useSnackbar } from "notistack";
export default function Notification(props) {
const { enqueueSnackbar } = useSnackbar();
const { messages } = props;
const options = {
variant: "success",
autoHideDuration: 6000,
transitionDuration: { enter: 400, exit: 400 },
};
messages.forEach((msg) => {
enqueueSnackbar(msg, options);
});
return <></>;
}

I've solved the issue by wrapping up the App with the Notification provider:
import React from "react";
import { useSnackbar } from "notistack";
import Button from "#mui/material/Button";
import { SnackbarProvider } from "notistack";
import Typography from "#mui/material/Typography";
function MakeGlobal() {
const { enqueueSnackbar } = useSnackbar();
window.enqueueSnackbar = enqueueSnackbar;
return null;
}
export function displayNotification(messages) {
const options = {
variant: "success",
autoHideDuration: 6000,
transitionDuration: { enter: 400, exit: 400 },
};
messages.forEach((msg) => {
window.enqueueSnackbar(msg, options);
});
}
export function Provider({ children }) {
const notistackRef = React.createRef();
const onClickDismiss = (key) => () => {
notistackRef.current.closeSnackbar(key);
};
return (
<SnackbarProvider
maxSnack={4}
ref={notistackRef}
dense
preventDuplicate
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
sx={{
width: 700,
}}
style={{
fontSize: 15,
fontWeight: 700,
}}
action={(key) => (
<Button onClick={onClickDismiss(key)}>
<Typography
variant="subtitle2"
style={{
fontWeight: 600,
fontSize: 16,
color: "#00579b",
}}
>
Dismiss
</Typography>
</Button>
)}
>
<MakeGlobal />
{children}
</SnackbarProvider>
);
}
App.js
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<BrowserRouter>
<snackbar.Provider>
<AppRoutes />
<CssBaseline />
</snackbar.Provider>
</BrowserRouter>
);

Related

I have a problem with my provider to render different navigator stacks

I am implementing a provider which helps me to have the state of my user in different views, the main function of this provider is to render different one or the other stack navigator depending on whether the variable is full or empty, this in order to be able to generate two groups of screens depending on whether the user is authenticated or not.
Here my code:
View router.tsx:
import { NavigationContainer } from "#react-navigation/native"
import React, { useContext, useEffect, useState, useRef } from "react"
import { UserContext } from "./context/Usuario"
import AuthStack from "./routes/AuthStack"
import GeneralStack from "./routes/GeneralStack"
const Router = () => {
const { me } = useContext(UserContext)
const auth = useRef(false)
useEffect(() => {
return () => {
auth.current = me !== null
console.log("Hola")
}
}, [me])
return (
<NavigationContainer>
{auth.current ? <GeneralStack /> : <AuthStack />}
</NavigationContainer>
)
}
export default Router
Provider user,js:
import React, { useEffect, createContext, useState } from "react"
import AsyncStorage from "#react-native-async-storage/async-storage"
export const UserContext = createContext()
const UserProvider = ({ children }) => {
const [me, setMe] = useState(undefined)
const validStorage = async () => {
try {
const miSesion = await AsyncStorage.getItem("sesion")
console.log(miSesion)
setMe(JSON.parse(miSesion))
} catch (error) {
console.log(`ERROR: ${error.message}`)
}
}
useEffect(() => {
validStorage()
}, [])
return (
<UserContext.Provider value={{ me, setMe }}>
{children}
</UserContext.Provider>
)
}
export default UserProvider
GeneralStack:
import { createNativeStackNavigator } from "#react-navigation/native-stack"
import React from "react"
import TabStack from "./TabStack"
//import TabStack from "./TabStack"
const GeneralScreen = createNativeStackNavigator()
const GeneralStack = () => {
return (
<GeneralScreen.Navigator screenOptions={{ headerShown: false }}>
<GeneralScreen.Screen name="Tabs" component={TabStack} />
</GeneralScreen.Navigator>
)
}
export default GeneralStack
AuthStack:
import { createNativeStackNavigator } from "#react-navigation/native-stack"
import React from "react"
import Login from "../Screens/Login"
import Registro from "../Screens/Registro/Registro"
import SplashScreen from "../SplashScreen"
const AuthScreen = createNativeStackNavigator()
const AuthStack = () => {
return (
<AuthScreen.Navigator
initialRouteName="Splash"
screenOptions={{ headerShown: false }}>
<AuthScreen.Screen name="Login" component={Login} />
<AuthScreen.Screen name="Register" component={Registro} />
<AuthScreen.Screen name="Splash" component={SplashScreen} />
</AuthScreen.Navigator>
)
}
export default AuthStack
Login:
import React, { useState, useContext } from "react"
import {
Image,
ScrollView,
StatusBar,
Text,
TouchableOpacity,
View,
} from "react-native"
import { useNavigate } from "../../Hooks/useNavigate"
import MyTextInput from "../../components/MyTextInput"
import colors from "../../styles/colors"
import { loginStyles } from "../../styles/styles"
import { UserContext } from "../../context/Usuario"
import AsyncStorage from "#react-native-async-storage/async-storage"
export default function Login() {
const [, setIsSession] = useState(false)
const { setMe } = useContext(UserContext)
const navigate = useNavigate()
const [hidePassword, sethidePassword] = React.useState(false)
const [user] = useState({ user: "admin", password: "admin123" })
const [form, setForm] = useState({ user: "", password: "" })
const getStorage = async () => {
if (await AsyncStorage.getItem("sesion")) {
setIsSession(true)
} else {
setIsSession(false)
}
}
const signIn = async () => {
try {
console.log(user)
if (form.user === user.user && form.password === user.password) {
await AsyncStorage.setItem("sesion", JSON.stringify(form))
setMe(form)
setIsSession(true)
}
} catch (error) {
console.error(error)
}
}
const closeSesion = async () => {
await AsyncStorage.removeItem("sesion")
getStorage()
}
return (
<ScrollView contentContainerStyle={[loginStyles.container]}>
<StatusBar backgroundColor={colors.PURPLE} translucent={true} />
<View style={loginStyles.logo}>
<Image
source={require("../../recursos/images/Logo.png")}
style={{ height: 250, width: 250 }}
/>
</View>
<MyTextInput
onChangeText={(text: string) => {
setForm(state => ({ ...state, user: text }))
}}
keyboardType="email-address"
placeholder="E-mail"
/>
<MyTextInput
onChangeText={(text: string) => {
setForm(state => ({ ...state, password: text }))
}}
keyboardType={null}
placeholder="Contraseña"
bolGone={true}
secureTextEntry={hidePassword}
onPress={() => sethidePassword(!hidePassword)}
/>
<View style={loginStyles.btnMain}>
<TouchableOpacity onPress={signIn}>
<Text style={loginStyles.btntxt}>Iniciar Sesión</Text>
</TouchableOpacity>
</View>
<View style={loginStyles.btnTransparent}>
<TouchableOpacity
onPress={() => navigate({ screen: "Register" })}>
<Text
style={[loginStyles.btntxt, { color: colors.PURPLE }]}>
Registrarse
</Text>
</TouchableOpacity>
</View>
<View>
<TouchableOpacity>
<Text
style={[
loginStyles.txtTransparent,
{ textDecorationLine: "none" },
]}>
Olvide mi contraseña
</Text>
</TouchableOpacity>
</View>
</ScrollView>
)
}
Home:
import AsyncStorage from "#react-native-async-storage/async-storage"
import React, { useState } from "react"
import { Image, ScrollView, TouchableOpacity } from "react-native"
import { Calendar } from "react-native-calendars"
import { Text } from "react-native-elements"
import { useNavigate } from "../../Hooks/useNavigate"
import colors from "../../styles/colors"
const Home = () => {
const [showModal, setShowModal] = useState(false)
const [date, setDate] = useState<string>()
const navigate = useNavigate()
const [isSession, setIsSession] = useState(false)
const getStorage = async () => {
const data = await AsyncStorage.getItem("sesion")
console.log(data)
if (data) {
setIsSession(true)
} else {
setIsSession(false)
}
}
const closeSesion = async () => {
await AsyncStorage.removeItem("sesion")
getStorage()
}
return (
<ScrollView>
<TouchableOpacity onPress={() => closeSesion()}>
<Text>Hola porque soy bien molon</Text>
</TouchableOpacity>
<Image
source={require("../../recursos/images/coralio_logo.png")}
style={{
marginTop: 40,
height: 110,
width: "80%",
justifyContent: "center",
alignSelf: "center",
}}
/>
<Calendar
theme={{
selectedDayBackgroundColor: colors.BLACK,
arrowColor: colors.WHITE,
monthTextColor: colors.WHITE,
}}
style={{
backgroundColor: colors.PURPLE,
borderRadius: 10,
elevation: 4,
marginTop: 60,
margin: 10,
height: 400,
}}
onDayPress={day => {
console.log(day.dateString)
setDate(day.dateString)
setShowModal(false)
}}
onMonthChange={() => {}}
initialDate={"2023-01-16"}
minDate={new Date()
.toLocaleDateString("es-US", {
year: "numeric",
month: "2-digit",
day: "numeric",
formatMatcher: "basic",
})
.split("/")
.reverse()
.join("-")}
markedDates={{
day: {
marked: true,
dotColor: colors.WHITE,
selected: true,
selectedColor: colors.PURPLE,
},
}}
//maxDate={"2023-12-31"}
//hideExtraDays={false}
//disableArrowLeft={true}
//disableArrowRight={true}
//hideArrows={true}
//hideDayNames={true}
/>
</ScrollView>
)
}
export default Home
The problem I have is when doing the close session in home, if in login it sends me from the authstack view to generalstack, when I do the close session it doesn't send me back to login, but it does clean the state of the variable from asyncstorage.
Help :(
It looks like you're not clearing the me variable in your context when the user ends the session. I think your closeSesion method should look like this:
const closeSesion = async () => {
setMe(null)
await AsyncStorage.removeItem("sesion")
getStorage()
}

Why is there an Agora video call issue : "Can't publish stream, haven't joined yet!" while it's in "await"?

So here is my code:
VideoCall.jsx:
import React, { useState, useEffect } from "react";
import { config, useClient, useMicrophoneAndCameraTracks, channelName} from "./settings.js";
import { Grid } from "#material-ui/core";
import Video from "./Video.jsx";
import Controls from "./Controls";
export default function VideoCall(props) {
const { setInCall } = props;
const [users, setUsers] = useState([]);
const [start, setStart] = useState(false);
const client = useClient();
const { ready, tracks } = useMicrophoneAndCameraTracks();
useEffect(() => {
let init = async (name) => {
client.on("user-published", async (user, mediaType) => {
await client.subscribe(user, mediaType);
if (mediaType === "video") {
setUsers((prevUsers) => {
return [...prevUsers, user];
});
}
if (mediaType === "audio") {
user.audioTrack.play();
}
});
client.on("user-unpublished", (user, mediaType) => {
if (mediaType === "audio") {
if (user.audioTrack) user.audioTrack.stop();
}
if (mediaType === "video") {
setUsers((prevUsers) => {
return prevUsers.filter((User) => User.uid !== user.uid);
});
}
});
client.on("user-left", (user) => {
setUsers((prevUsers) => {
return prevUsers.filter((User) => User.uid !== user.uid);
});
});
try {
await client.join(config.appId, name, config.token, null);
} catch (error) {
console.log("error");
}
debugger;
if (tracks) await client.publish([tracks[0], tracks[1]]);
setStart(true);
};
if (ready && tracks) {
try {
init(channelName);
} catch (error) {
console.log(error);
}
}
}, [channelName, client, ready, tracks]);
return (
<Grid container direction="column" style={{ height: "100%" }}>
<Grid item style={{ height: "5%" }}>
{ready && tracks && (
<Controls tracks={tracks} setStart={setStart} setInCall={setInCall} />
)}
</Grid>
<Grid item style={{ height: "95%" }}>
{start && tracks && <Video tracks={tracks} users={users} />}
</Grid>
</Grid>
);
}
Video.jsx:
import { AgoraVideoPlayer } from "agora-rtc-react";
import { Grid } from "#material-ui/core";
import React, { useState, useEffect } from "react";
export default function Video(props) {
const { users, tracks } = props;
const [gridSpacing, setGridSpacing] = useState(12);
useEffect(() => {
setGridSpacing(Math.max(Math.floor(12 / (users.length + 1)), 4));
}, [users, tracks]);
return (
<Grid container style={{ height: "100%" }}>
<Grid item xs={gridSpacing}>
<AgoraVideoPlayer
videoTrack={tracks[1]}
style={{ height: "100%", width: "100%" }}
/>
</Grid>
{users.length > 0 &&
users.map((user) => {
if (user.videoTrack) {
return (
<Grid item xs={gridSpacing}>
<AgoraVideoPlayer
videoTrack={user.videoTrack}
key={user.uid}
style={{ height: "100%", width: "100%" }}
/>
</Grid>
);
} else return null;
})}
</Grid>
);
}
Controls.jsx:
import React, { useState } from "react";
import { useClient } from "./settings";
import { Grid, Button } from "#material-ui/core";
import ExitToAppIcon from "#material-ui/icons/ExitToApp";
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import { faMicrophone, faMicrophoneSlash, faVideoSlash, faVideo } from "#fortawesome/free-solid-svg-icons";
import { useTranslation } from 'react-i18next';
export default function Controls(props) {
const {t} = useTranslation();
const client = useClient();
const { tracks, setStart, setInCall } = props;
const [trackState, setTrackState] = useState({ video: true, audio: true });
const mute = async (type) => {
if (type === "audio") {
await tracks[0].setEnabled(!trackState.audio);
setTrackState((ps) => {
return { ...ps, audio: !ps.audio };
});
} else if (type === "video") {
await tracks[1].setEnabled(!trackState.video);
setTrackState((ps) => {
return { ...ps, video: !ps.video };
});
}
};
const leaveChannel = async () => {
await client.leave();
client.removeAllListeners();
tracks[0].close();
tracks[1].close();
setStart(false);
setInCall(false);
};
return (
<Grid container spacing={2} alignItems="center">
<Grid item>
<Button variant="contained" color={trackState.audio ? "primary" : "secondary"} onClick={() => mute("audio")} >
{trackState.audio ? <FontAwesomeIcon icon={faMicrophone} /> : <FontAwesomeIcon icon={faMicrophoneSlash} />}
</Button>
</Grid>
<Grid item>
<Button variant="contained" color={trackState.video ? "primary" : "secondary"} onClick={() => mute("video")} >
{trackState.video ? <FontAwesomeIcon icon={faVideo} /> : <FontAwesomeIcon icon={faVideoSlash} />}
</Button>
</Grid>
<Grid item>
<Button variant="contained" color="default" onClick={() => leaveChannel()} >
{t('agora.leave')}
<ExitToAppIcon />
</Button>
</Grid>
</Grid>
);
}
settings.js:
import {createClient, createMicrophoneAndCameraTracks} from 'agora-rtc-react';
export const config = {mode: 'rtc', codec: "vp8", appId: process.env.AGORA_APP_ID, token : process.env.AGORA_TOKEN};
export const useClient = createClient(config);
export const useMicrophoneAndCameraTracks = createMicrophoneAndCameraTracks();
export const channelName = process.env.AGORA_CHANNEL_NAME;
AgoraDemo.jsx:
import React, {useState} from "react";
import {Form, Modal, Button,Row, Col} from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import VideoCall from "./agora/VideoCall";
const AgoraDemo = ({subjects}) => {
const {t} = useTranslation();
const [subjectSelected, setSubjectSelected] = useState(null);
const [inCall, setInCall] = useState(false);
return (
<Modal.Dialog size='xl'>
<Modal.Header style={{ display: 'flex', justifyContent: 'center', alignItems: 'center'}}>
<Modal.Title>
{t('agora.headline')}
</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className="App" style={{ height: "100%" }}>
{subjectSelected && inCall ? (
<VideoCall setInCall={setInCall} />
) : (
<Button onClick={() => setInCall(true)}>
{t('agora.join call')}
</Button>
)}
</div>
</Modal.Body>
<Modal.Footer/>
</Modal.Dialog>
)
}
export default AgoraDemo;
I don't know much plain javascript so I don't understand why this await does not pass first before proceding to the next line of code and how I should fix it. This is the error:
×
Unhandled Rejection (AgoraRTCException): AgoraRTCError INVALID_OPERATION: Can't publish stream, haven't joined yet!
▶ 2 stack frames were collapsed.
async init
C:/Users/User/Desktop/Agora/src/agora/VideoCall.jsx:53
50 |
51 | debugger;
52 |
53 | if (tracks) await client.publish([tracks[0], tracks[1]]);
| ^ 54 | setStart(true);
55 | };
56 |
The implementation was working. The problem was with the token. It turned out it expires (but didn't get afteh how long exactly) and then you get the given error. For me the solution was to obtain a token for every video call. This way I don't need to renew it manually every time it expires :)

How to change prop of Material UIs Button component based on screen size breakpoint when using a class component

Im using Material UI's Button component and would like to conditionally determine the variant of the button. That is, on screens medium and up, the variant should be 'outlined' and when the screen is smaller than medium, the variant type should be none. I am using a class component. I have done my research to see what others have done. I have seen the method of using useMediaQuery method but that would require me to change the component to a functional component. Im not sure if I know if that is a good idea because the component is rather a large one and that means will be a bit confusing to convert. I also tried using a ternary operator like this:
const variantType = theme.breakpoints.down("md") ? '' : 'outline';
<Button variant={variantType}>
Food Pick
</Button>
But this didnt do the job. Is there any other way to accomplish this?
Update: Here is my code after trying to wrap the component but in a functional component but its giving an error:
import { withStyles, withTheme, useTheme } from '#material-ui/core';
import PropTypes from 'prop-types';
import Button from '#material-ui/core/Button';
import Typography from '#material-ui/core/Typography';
import Grid from '#material-ui/core/Grid';
import { PulseLoader } from 'react-spinners';
import Icon from '#material-ui/core/Icon';
import Chip from '#material-ui/core/Chip';
import useMediaQuery from '#material-ui/core/useMediaQuery';
const styles = theme => ({
beta: {
height: '17px',
fontSize: '12px',
marginLeft: '10px'
},
scrollContainer: {
flex: 1,
maxHeight: '400px',
minHeight: '300px',
overflowY: 'auto'
},
progressContainer: {
height: '350px',
},
modalDescription: {
color: theme.palette.text.secondary,
marginTop: '20px',
},
button: {
marginTop: '20px',
marginLeft: '10px',
marginRight: '10px',
},
smartSuggestContainer: {
textAlign: 'right',
paddingRight: '35px',
[theme.breakpoints.down('xs')]: {
display: 'flex',
justifyContent: 'center',
flexDirection: 'row',
margin: '40px 0'
},
},
});
export default function MyComponentWrapper({ ...rest }) {
const theme = useTheme();
const mediumScreen = useMediaQuery(theme.breakpoints.down('md'));
return <FoodDescriptions {...rest} mediumScreen={mediumScreen} />;
}
class FoodDescriptions extends Component {
static PAGE_SIZE = 25;
constructor(props) {
super(props);
this.state = {
showFoodDescriptionsModal: false,
dataLoaded: false,
data: [],
page: 0,
sortBy: 'return',
sortDir: 'desc',
};
}
componentDidMount() {
document.addEventListener('keydown', this.handleKeypress);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.handleKeypress);
}
fetchData = async (page, sortBy, sortDir) => {
const { currentBotId } = this.props;
const offset = page * FoodDescriptions.PAGE_SIZE;
const data = await getFoodDescriptions(currentBotId, sortBy, sortDir, FoodDescriptions.PAGE_SIZE, offset);
this.setState({
dataLoaded: true,
data,
});
};
handleKeypress = (e) => {
const { showSnartConfigsModal } = this.state;
const { key } = e;
if (key === 'Escape' && showSnartConfigsModal) {
this.closeModal();
}
};
applyConfig = (botId, params) => {
const { updateConfig, botConfig, actions } = this.props;
updateConfig({ name: botConfig.name, config: Object.assign(botConfig.config, params) });
this.closeModal();
actions.showNotification({ data: 'Configuration has been applied' });
};
openModal = () => {
const { page, sortBy, sortDir } = this.state;
this.fetchData(page, sortBy, sortDir);
this.setState({
showFoodDescriptionsModal: true,
});
};
closeModal = () => {
this.setState({
showFoodDescriptionsModal: false,
});
};
changePage = (page) => {
const { sortBy, sortDir } = this.state;
this.setState({
page,
dataLoaded: false
}, () => this.fetchData(page, sortBy, sortDir));
};
changeSortBy = (sortBy) => {
/* eslint-disable-next-line prefer-destructuring */
let sortDir = this.state.sortDir;
if (sortBy === this.state.sortBy) {
sortDir = (this.state.sortDir === 'desc') ? 'asc' : 'desc';
}
this.setState({
sortBy,
sortDir,
dataLoaded: false,
}, () => this.fetchData(this.state.page, sortBy, sortDir));
};
renderEmptyState() {
const { classes } = this.props;
return (
<Grid container alignItems="center" justify="center" className={classes.progressContainer}>
<Typography className={classes.noCoinsText}>No configurations found</Typography>
</Grid>
);
}
renderLoader() {
const { classes } = this.props;
return (
<Grid container alignItems="center" justify="center" className={classes.progressContainer}>
<PulseLoader size={6} color="#52B0B0" loading />
</Grid>
);
}
renderTable() {
const { data, page } = this.state;
return (
<StrategiesTable
strategies={data}
onClickCopy={this.applyConfig}
page={page}
changePage={this.changePage}
sortBy={this.changeSortBy} />
);
}
render() {
const {
classes, userFeatures, botConfig, theme
} = this.props;
const { showFoodDescriptionsModal, dataLoaded } = this.state;
if (!botConfig) {
return null;
}
return (
<Fragment>
<div className={classes.smartSuggestContainer}>
<Button
name="discoverConfigs"
variant={theme.breakpoints.down(600) ? '' : 'outlined'}
color="primary"
size="small"
disabled={!userFeatures['smart_suggest_backtests'.toUpperCase()] || botConfig.status.toLowerCase() === STATUSES.RUNNING}
onClick={this.openModal}>
Food Buy
</Button>
</div>
</Fragment>
);
}
}
function mapStateToProps(state) {
return {
userFeatures: state.global.paywall.features,
};
}
function mapDispatchToProps(dispatcher) {
return {
actions: {
...bindActionCreators({
showNotification,
}, dispatcher)
}
};
}
connect(mapStateToProps, mapDispatchToProps)(withTheme()(withStyles(styles)(FoodDescriptions)));
Why don't you just create a functional component using useMediaQuery and returning the responsive Button ?

React, how do I persist the state of side drawer selected list item with router? [duplicate]

This question already has an answer here:
How do you get Material-UI Drawer to highlight the page it is currently at?
(1 answer)
Closed 1 year ago.
I'm using react-router and material UI. I integrated the routes with my material UI persist drawer list items with some custom styling like when I click on a list item it highlights it. But I'm facing an issue when I refresh the page my selected list item gets reset, even though I'm still on the same page. Can anyone tell me how do I persist the selected list item even if a page gets refreshed?
Inshort: Issue- when I refresh the page my drawer list item selected color get reset to top item even though I'm on the same page.
Here is the gif to demonstrate my issue
Here is my sandbox code link
Below is the code for the same
Note: I would suggest you go through my code sandbox it'll be better for you to lookup in code. Thank you
App.js
import { Switch, Route } from "react-router-dom";
import Login from "./pages/Login";
import Dashboard from "./pages/Dashboard";
import "./styles.css";
export default function App() {
return (
<div className="App">
<Switch>
<Route path="/dashboard" component={Dashboard} />
<Route path="/" exact component={Login} />
</Switch>
</div>
);
}
Dashboard.jxs for nested routes in dashboard
import { Switch, Redirect, Route, useRouteMatch } from "react-router-dom";
import Home from "./Home";
import About from "./About";
import NavBar from "../components/NavBar";
const Dashboard = () => {
const { path } = useRouteMatch();
return (
<>
<NavBar>
<Switch>
<Route path={`${path}/about`} component={About} />
<Route path={`${path}/home`} component={Home} />
<Redirect to={`${path}/home`} />
</Switch>
</NavBar>
</>
);
};
export default Dashboard;
NavBar.js
import React from "react";
import clsx from "clsx";
import { makeStyles, useTheme } from "#material-ui/core";
import CssBaseline from "#material-ui/core/CssBaseline";
import AppBar from "#material-ui/core/AppBar";
import Toolbar from "#material-ui/core/Toolbar";
import Typography from "#material-ui/core/Typography";
import IconButton from "#material-ui/core/IconButton";
import MenuIcon from "#material-ui/icons/Menu";
import { useRouteMatch } from "react-router-dom";
import SideDrawer from "./SideDrawer";
const drawerWidth = 240;
const useStyles = makeStyles((theme) => ({
root: {
display: "flex"
},
appBar: {
transition: theme.transitions.create(["margin", "width"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
})
},
appBarShift: {
width: `calc(100% - ${drawerWidth}px)`,
marginLeft: drawerWidth,
transition: theme.transitions.create(["margin", "width"], {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen
})
},
menuButton: {
marginRight: theme.spacing(2)
},
hide: {
display: "none"
},
drawer: {
width: drawerWidth,
flexShrink: 0
},
drawerPaper: {
width: drawerWidth
},
drawerHeader: {
display: "flex",
alignItems: "center",
padding: theme.spacing(0, 1),
// necessary for content to be below app bar
...theme.mixins.toolbar,
justifyContent: "flex-end"
},
content: {
flexGrow: 1,
padding: theme.spacing(3),
transition: theme.transitions.create("margin", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
}),
marginLeft: -drawerWidth
},
contentShift: {
transition: theme.transitions.create("margin", {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen
}),
marginLeft: 0
}
}));
export default function NavBar({ children }) {
const classes = useStyles();
const theme = useTheme();
const [open, setOpen] = React.useState(true);
const { url } = useRouteMatch();
const handleDrawerOpen = () => {
if (!open) {
setOpen(true);
} else {
setOpen(false);
}
};
return (
<div className={classes.root}>
<CssBaseline />
<AppBar
position="fixed"
className={clsx(classes.appBar, {
[classes.appBarShift]: open
})}
>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
onClick={handleDrawerOpen}
edge="start"
className={clsx(classes.menuButton)}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap>
Persistent drawer
</Typography>
</Toolbar>
</AppBar>
{/* child compoent to render side drawer and state is passing to open & close */}
<SideDrawer open={open} />
<main
className={clsx(classes.content, {
[classes.contentShift]: open
})}
>
<div className={classes.drawerHeader} />
{children}
</main>
</div>
);
}
SideDrawer.js its a child component inside NavBar.js
import {
useTheme,
Divider,
Drawer,
IconButton,
List,
ListItem,
ListItemIcon,
makeStyles
} from "#material-ui/core";
import React from "react";
import ChevronLeftIcon from "#material-ui/icons/ChevronLeft";
import ChevronRightIcon from "#material-ui/icons/ChevronRight";
import DrawerList from "./DrawerList";
const drawerWidth = 240;
const useStyles = makeStyles((theme) => ({
drawer: {
width: drawerWidth,
flexShrink: 0
},
drawerPaper: {
width: drawerWidth
},
drawerHeader: {
display: "flex",
alignItems: "center",
padding: theme.spacing(0, 1),
// necessary for content to be below app bar
...theme.mixins.toolbar,
justifyContent: "flex-end"
}
}));
const SideDrawer = (props) => {
const theme = useTheme();
const classes = useStyles();
const { open } = props;
return (
<Drawer
className={classes.drawer}
variant="persistent"
anchor="left"
open={open}
classes={{
paper: classes.drawerPaper
}}
>
<div className={classes.drawerHeader}>
<h1>Header</h1>
</div>
<Divider />
<List>
{/* my component to render list of icons in side drawer */}
<DrawerList />
</List>
</Drawer>
);
};
export default SideDrawer;
DrawerList.js child component of SideDrawer.js Issue is arising here
import React, { useState } from "react";
import ListItem from "#material-ui/core/ListItem";
import ListItemIcon from "#material-ui/core/ListItemIcon";
import { DashboardOutlined, AddCircleOutline } from "#material-ui/icons";
import ListItemText from "#material-ui/core/ListItemText";
import { makeStyles, Typography } from "#material-ui/core";
import Box from "#material-ui/core/Box";
import { Link, useRouteMatch } from "react-router-dom";
const useStyles = makeStyles((theme) => ({
root: {
marginTop: theme.spacing(1)
},
iconStyle: {
margin: theme.spacing(0, 0),
color: "#676767"
},
iconTitle: {
margin: theme.spacing(0, 0, 0, 1),
color: "#676767"
},
listItem: {
"&.Mui-selected": {
// it is used to change external svg color during click
"& path": {
fill: "#fff"
},
margin: theme.spacing(1.5, 1)
}
}
}));
const DrawerList = ({ children }) => {
console.log(children);
const [selectedIndex, setSelectedIndex] = useState(0);
const classes = useStyles();
const { url } = useRouteMatch();
const itemList = [
{
text: "Home",
icon: <DashboardOutlined />,
keys: "home",
to: `${url}/home`
},
{
text: "Appointment",
icon: <AddCircleOutline />,
keys: "about",
to: `${url}/about`
}
];
const ListData = () =>
itemList.map((item, index) => {
const { text, icon, to, keys } = item;
return (
<ListItem
className={classes.listItem}
button
key={keys}
to={to}
component={Link}
selected={index === selectedIndex}
onClick={(e) => handleListItemClick(e, index)}
style={
selectedIndex === index
? {
background: "#3f51b5",
width: 200,
marginLeft: 8,
paddingLeft: 10,
borderRadius: 4,
boxShadow: "2px 3px 6px rgba(0, 0, 0, 0.3)"
}
: {}
}
>
<ListItemIcon
className={classes.iconStyle}
style={selectedIndex === index ? { color: "#fff" } : {}}
>
{icon}
<ListItemText>
<Typography
component="div"
className={classes.iconTitle}
style={selectedIndex === index ? { color: "#fff" } : {}}
>
<Box fontWeight={700} fontSize={13.8}>
{text}
</Box>
</Typography>
</ListItemText>
</ListItemIcon>
</ListItem>
);
});
const handleListItemClick = (e, index) => {
setSelectedIndex(index);
};
return (
<div className={classes.root}>
<ListData />
</div>
);
};
export default DrawerList;
You need to track the url to show the selected items instead of a useState that gets reseted:
const { url } = useRouteMatch()
const {pathname} = useLocation();
const itemList = [
{
text: "Home",
icon: <DashboardOutlined />,
keys: "home",
to: `${url}/home`
},
{
text: "About",
icon: <AddCircleOutline />,
keys: "about",
to: `${url}/about`
}
];
...
selected={pathname === to}
To persist data between page refresh, you can use localStorage API.
You need to initialize your state with the value from localStorage. And whenever you update your react state, you also need to update the value in localStorage, so that after page is refreshed, your component state gets initialized with this stored value.
For eg:
const storedOpen = JSON.parse(localStorage.getItem('drawerOpen'));
const [open, setOpen] = React.useState(storedOpen);
const handleDrawerOpen = () => {
if (!open) {
setOpen(true);
localStorage.setItem('drawerOpen', true);
} else {
setOpen(false);
localStorage.setItem('drawerOpen', false);
}
};

Rendering multiple input fields causing lag after 200 items

I have a project where i can have n-number of input field. After when i add 200 items, it starts lagging after that. Demo : https://testcreate.vivekneel.now.sh/create (To test: Focus last input and try pressing enter button to create new input)
Source Code. : https://github.com/VivekNeel/Create-Test
This is my main container :
import React, { useState } from "react";
import CreateTerms from "./CreateTerms";
import { initTerms, contructTermObject } from "./utils";
import { Container } from "#material-ui/core/";
const CreateStudySetContainer = () => {
const [terms, setTerms] = useState(initTerms);
const [inputIdToFocus, setInputIdToFocus] = useState(null);
const handleCreateTerm = () => {
const newTerm = contructTermObject(terms.length + 1, 2);
const newTerms = [...terms, newTerm];
setInputIdToFocus(terms.length + 1);
setTerms(newTerms);
};
console.log("....rendering");
return (
<Container maxWidth={"md"}>
<CreateTerms
terms={terms}
inputIdToFocus={inputIdToFocus}
createTerm={handleCreateTerm}
/>
;
</Container>
);
};
export default CreateStudySetContainer;
This the CreateTerms code :
import React from "react";
import CreateFacts from "./CreateFacts";
import { withStyles, Card } from "#material-ui/core/";
import ContentItemRow from "./ContentItemRow";
const styles = () => ({
card: {
marginBottom: 16,
},
});
const CreateTerms = (props) => {
const { terms, classes, createTerm, inputIdToFocus } = props;
return (
<div className={classes.container}>
{terms.map(({ node: { term } }, index) => {
return (
<Card key={term.id} className={classes.card}>
<p>{index}</p>
<ContentItemRow
autoFocus={term.id === inputIdToFocus}
createTerm={createTerm}
term={term}
/>
;
</Card>
);
})}
</div>
);
};
export default withStyles(styles)(CreateTerms);
This is ContentItemRow :
import React from "react";
import CreateFacts from "./CreateFacts";
import { withStyles } from "#material-ui/core/";
import ContentEditor from "./ContentEditor";
const styles = {
container: {
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "flex-start",
},
term: {
marginRight: 16,
flex: 1,
},
facts: {
flex: 1,
},
};
const ContentItemRow = (props) => {
const { term, classes, createTerm, autoFocus } = props;
return (
<div className={classes.container}>
<div className={classes.term}>
<ContentEditor
autoFocus={autoFocus}
createTerm={createTerm}
placeholder={"New term"}
/>
</div>
<div className={classes.facts}>
{term.facts.map(({ fact }) => {
return (
<ContentEditor
key={fact.id}
createTerm={createTerm}
placeholder={"New fact"}
/>
);
})}
</div>
</div>
);
};
export default withStyles(styles)(ContentItemRow);
This is ContentEditor :
import React from "react";
import { TextField } from "#material-ui/core/";
const ContentEditor = (props) => {
const { placeholder, createTerm, autoFocus } = props;
const handleOnKeyDown = (event) => {
const { keyCode } = event;
if (keyCode === 13) {
createTerm();
}
};
return (
<TextField
onKeyDown={handleOnKeyDown}
fullWidth
autoFocus={autoFocus}
placeholder={placeholder}
/>
);
};
export default ContentEditor;
When debugging, I noticed that dom updates only the last div which gets added. I don't know where the lagging is coming from.
Not sure if this will work but you can try the following:
const ContentItemRow = React.memo(function ContentItemRow (props) => {
And to prevent re creating the create term handler:
const handleCreateTerm = useCallback(() => {
setTerms((terms) => {
const newTerm = contructTermObject(
terms.length + 1,
2
);
const newTerms = [...terms, newTerm];
setInputIdToFocus(terms.length + 1);
return newTerms;
});
}, []);

Categories

Resources