I'm trying to apply the theme the user chose to the initial value of useState(), but when I refresh the page, the choice does not apply. What do I have to change in order for the value to persist through page refreshing?
theme-toggler.js
import React, { createContext, useState } from "react";
export const themes = {
light: {
background: "#41A9EC",
fontColor: '#FFF'
},
dark: {
background: "#F9F9",
fontColor: '#000'
}
}
export const ThemeContext = createContext({})
export const ThemeProvider = (props) => {
const [theme, setTheme] = useState(localStorage.themes)
if(theme === themes.light) {
localStorage.setItem('themes', JSON.stringify(themes.light))
}
if(theme === themes.dark) {
localStorage.setItem('themes', JSON.stringify(themes.dark))
}
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{props.children}
</ThemeContext.Provider>
)
}
theme-toggler-button.js
import React, { useContext } from "react"
import { ThemeContext, themes } from "../../context/theme-toggler"
import { Button } from "../button/button"
export const ThemeTogglerButton = () => {
const { theme, setTheme } = useContext(ThemeContext)
return (
<div style={{ backgroundColor: theme.background, color: theme.fontColor }}>
<Button onClick={() => setTheme(theme === themes.light ? themes.dark : themes.light)}>Theme Toggler</Button>
</div>
)
}
Thanks in advance.
import React, { createContext, useState } from "react";
export const themes = {
light: {
background: "#41A9EC",
fontColor: '#FFF'
},
dark: {
background: "#F9F9",
fontColor: '#000'
}
}
export const ThemeContext = createContext({})
export const ThemeProvider = (props) => {
const [theme, setTheme] = useState(localStorage.getItem("themes"))
useEffect(() => {
if(theme === themes.light) {
localStorage.setItem('themes', JSON.stringify(themes.light))
}
if(theme === themes.dark) {
localStorage.setItem('themes', JSON.stringify(themes.dark))
}
},[theme])
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{props.children}
</ThemeContext.Provider>
)
}
After a few days, I was able to make it work. I'm posting the solution I found, in order to help others with similar problems.
theme-toggler-button file:
import React, { useContext } from "react"
import { ThemeContext, themes } from "../../context/theme-toggler"
import { Button } from "../button"
export const ThemeTogglerButton = () => {
const { theme, setTheme } = useContext(ThemeContext)
function handleClick() {
const localTheme = JSON.parse(localStorage.getItem("themes"))
console.log(localTheme)
setTheme(theme === themes.light ? themes.dark : themes.light)
if (localTheme) {
localStorage.setItem('themes', JSON.stringify(localTheme.name === 'light mode' ? themes.dark : themes.light))
} else {
localStorage.setItem('themes', JSON.stringify(themes.light))
}
}
return (
<Button style={{ backgroundColor: theme.background,
color: theme.fontColor }}
onClick={() => handleClick()}>{
(theme === themes.light ?
themes.dark.name : themes.light.name)}
</Button>
)
}
theme-toggler file:
import React, { createContext, useState, useEffect } from "react";
export const themes = {
light: {
name: 'light mode',
background: '#41A9EC',
fontColor: '#FFF'
},
dark: {
name: 'dark mode',
background: '#212121',
fontColor: '#AAB0BC'
}
}
export const ThemeContext = createContext({})
export const ThemeProvider = (props) => {
const [theme, setTheme] = useState([])
useEffect(() => {
const localTheme = JSON.parse(localStorage.getItem("themes"))
if (!localTheme) {
localStorage.setItem('themes', JSON.stringify(themes.light))
setTheme(themes.light)
}
if (localTheme) {
if (localTheme.name === 'light mode') {
localStorage.setItem('themes', JSON.stringify(themes.light))
setTheme(themes.light)
}
if (localTheme.name === 'dark mode') {
localStorage.setItem('themes', JSON.stringify(themes.dark))
setTheme(themes.dark)
}
}
}, [])
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{props.children}
</ThemeContext.Provider>
)
}
Please find below my project repository, where I'm currently using the solution above: https://github.com/Alex-Lima84/pokemon-react-api
Related
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()
}
I'm trying to switch this line of code to an arrow function:
<NavigationContainer theme={theme == 'Amber' ? Amber : Tiger}
<NavigationContainer theme={() => {
if (theme == 'Amber') {
return Amber;
} else {
return Tiger;
}
}}>
But for some reason when I perform this change, I get this error: TypeError: undefined is not an object (evaluating 'colors.background')
What's the problem here?
Thanks
Full code for reference:
import React, { useState } from 'react';
import { NavigationContainer, useTheme } from '#react-navigation/native';
import { StatusBar } from 'expo-status-bar';
import { createBottomTabNavigator } from '#react-navigation/bottom-tabs';
import * as NavigationBar from "expo-navigation-bar";
import Ionicons from 'react-native-vector-icons/Ionicons';
import CounterScreen from './screens/CounterScreen';
import CustomizeScreen from './screens/CustomizeScreen';
import SettingsScreen from './screens/SettingsScreen';
import MedalsScreen from './screens/MedalsScreen.js';
import { Amber, Tiger } from "../styles/Themes"
import { ThemeContext } from '#rneui/themed';
const counterName = 'Counter';
const customizeName = 'Customize';
const settingsName = 'Settings';
const medalsName = "Medals";
const Tab = createBottomTabNavigator();
NavigationBar.setBackgroundColorAsync("#212121");
export default function MainContainer() {
const [theme, setTheme] = useState('Amber');
const themeData = { theme, setTheme };
return (
<ThemeContext.Provider value={themeData}>
<NavigationContainer theme={() => {
if (theme == 'Amber') {
return Amber;
} else {
return Tiger;
}
}}>
<StatusBar style="auto" />
<Tab.Navigator
initialRouteName={counterName}
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName;
let rn = route.name;
if (rn === counterName) {
iconName = focused ? 'radio-button-on-outline' : 'radio-button-off-outline';
} else if (rn === customizeName) {
iconName = focused ? 'color-palette' : 'color-palette-outline';
} else if (rn === medalsName) {
iconName = focused ? 'medal' : 'medal-outline';
} else if (rn === settingsName) {
iconName = focused ? 'settings' : 'settings-outline';
}
return <Ionicons name={iconName} size={size} color={color} />
},
tabBarInactiveTintColor: '#aaaaaa',
tabBarLabelStyle: { paddingBottom: 10, fontSize: 10 },
tabBarStyle: { padding: 10, height: 70, borderTopWidth: 1 },
headerStyle: { borderBottomWidth: 1 },
headerTitleAlign: 'center',
headerTitleStyle: { fontSize: 20 },
})}>
<Tab.Screen name={counterName} component={CounterScreen} />
<Tab.Screen name={customizeName} component={CustomizeScreen} />
<Tab.Screen name={medalsName} component={MedalsScreen} />
<Tab.Screen name={settingsName} component={SettingsScreen} />
</Tab.Navigator>
</NavigationContainer>
</ThemeContext.Provider>
);
}
Themes.js
const Amber = {
dark: true,
colors: {
primary: '#FFBF00',
background: '#212121',
card: '#212121',
text: '#FFBF00',
border: '#FFBF00',
notification: '#FFBF00',
},
};
const Tiger = {
dark: true,
colors: {
primary: '#F96815',
background: '#212121',
card: '#212121',
text: '#F96815',
border: '#F96815',
notification: '#F96815',
},
};
export { Amber, Tiger }
looks like its expecting an object, why not try using useMemo or a simple function the generated an object before setting the proprty
const containerTheme = useMemo(() => {
// logic to select theme
if (theme == 'Amber') {
return Amber;
} else {
return Tiger;
}
}, [theme])
<NavigationContainer theme={containerTheme}>
// simple function
const getTheme = () => {
// logic to select theme
if (theme == 'Amber') {
return Amber;
} else {
return Tiger;
}
}
const containerTheme = getTheme();
if your function is expensive check the time
console.time('get theme');
const containerTheme = getTheme();
console.timeEnd('get theme');
I am new to this so I hope this is the right place to get help!
As titled, executing this code is giving me the "Too many re-renders" error on React.
I have tried going through all lines and checking my hooks repeatedly, but nothing seems to work.
I am guessing this is happening due to useEffect, so pasting the code for the relevant components below:
UseResults:
import { useEffect, useState } from 'react';
import yelp from '../api/yelp';
export default () => {
const [results, setResults] = useState([]);
const [errorMessage, setErrorMessage] = useState('');
const searchApi = async () => {
try {
const response = await yelp.get('/search', {
params: {
limit: 50,
term,
location: 'san francisco'
}
});
setResults(response.data.businesses);
} catch (err) {
setErrorMessage('Something went wrong')
}
};
useEffect(() => {
searchApi('pasta');
}, []);
return [searchApi, results, errorMessage];
}
SearchScreen:
import React, { useState } from 'react';
import { Text, StyleSheet } from 'react-native';
import { ScrollView } from 'react-native-gesture-handler';
import ResultsList from '../components/ResultsList';
import SearchBar from '../components/SearchBar';
import useResults from '../hooks/useResults';
const SearchScreen = (navigation) => {
const [term, setTerm] = useState('');
const [searchApi, results, errorMessage] = useResults();
const filterResultsByPrice = (price) => {
return results.filter(result => {
return result.price === price;
});
};
return <>
<SearchBar
term={term}
onTermChange={setTerm}
onTermSubmit={searchApi()}
/>
{errorMessage ? <Text>{errorMessage}</Text> : null}
<Text>We have found {results.length} results</Text>
<ScrollView>
<ResultsList
results={filterResultsByPrice('$')}
title="Cost Effective"
navigation={navigation}
/>
<ResultsList
results={filterResultsByPrice('$$')}
title="Bit Pricier"
navigation={navigation}
/>
<ResultsList
results={filterResultsByPrice('$$$')}
title="Big Spender"
navigation={navigation}
/>
</ScrollView>
</>
};
const styles = StyleSheet.create({});
export default SearchScreen;
ResultsList:
import React from 'react';
import { View, Text, StyleSheet, FlatList } from 'react-native';
import { TouchableOpacity } from 'react-native-gesture-handler';
import ResultsDetail from './ResultsDetail';
const ResultsList = ({ title, results, navigation }) => {
return (
<View style={styles.container} >
<Text style={styles.title}>{title}</Text>
<FlatList
horizontal
showsHorizontalScrollIndicator={false}
data={results}
keyExtractor={result => result.id}
renderItem={({ item }) => {
return (
<TouchableOpacity onPress={() => navigation.navigate('ResultsShow')}>
<ResultsDetail result={item} />
</TouchableOpacity>
)
}}
/>
</View>
);
};
const styles = StyleSheet.create({
title: {
fontSize: 18,
fontWeight: 'bold',
marginLeft: 15,
marginBottom: 5
},
container: {
marginBottom: 10
}
});
export default ResultsList;
TIA!
I'm implementing a dark mode theme to understand React context. I have the same code for my heading component and it works fine. When I try to add the same for my main tag I get type error: _useContext is undefined.
import React, { useContext } from 'react';
import Heading from './heading/heading';
import ThemeToggle from './heading/themeToggle';
import ThemeContextProvider from './context/ThemeContex';
import './App.css';
import { ThemeContext } from './context/ThemeContex';
const App = () => {
const { light, dark, isLightTheme } = useContext(ThemeContext);
const theme = isLightTheme ? light : dark;
return (
<>
<ThemeContextProvider>
<div className="grid">
<>
<Heading />
<ThemeToggle />
</>
<main style={{ background: theme.bh, color: theme.color }}>
<div className="first-container">
<img src={require('./img/madeInAbyss.jpeg')} />
</div>
<div className="second-container"></div>
</main>
</div>
</ThemeContextProvider>
</>
);
};
export default App;
here is the context provider file which just has a color theme object and a state to toggle between dark and light mode
import React, { createContext, useState } from 'react';
export const ThemeContext = createContext();
const ThemeContextProvider = props => {
const [isLightTheme, setIsLightTheme] = useState(true);
const colorTheme = {
light: { ui: '#ddd', bg: '#eee' },
dark: { color: '#fff', bg: '#15202b' }
};
console.log(colorTheme);
const toggleTheme = () => {
setIsLightTheme(!isLightTheme);
};
return (
<ThemeContext.Provider
value={{
...colorTheme,
isLightTheme: isLightTheme,
toggleTheme: toggleTheme
}}>
{props.children}
</ThemeContext.Provider>
);
};
export default ThemeContextProvider;
You using ThemeContext value before it is initialized within ThemeContextProvider.
const App = () => {
// ThemeContext initial value is undefined (createContext())
// will throw a runtime error
const { light, dark, isLightTheme } = useContext(ThemeContext);
return (
<ThemeContextProvider>
{/* ThemeContext initialized only on ThemeContextProvider render */}
{/* after .Provider value is supplied */}
</ThemeContextProvider>
);
};
To fix it, provide an initial value:
// Should be in an outer scope.
const colorTheme = {
light: { ui: '#ddd', bg: '#eee' },
dark: { color: '#fff', bg: '#15202b' },
isLightTheme: true,
};
export const ThemeContext = createContext(colorTheme);
const ThemeContextProvider = props => {
...
return (
<ThemeContext.Provider
value={...}>
{props.children}
</ThemeContext.Provider>
);
};
When my reducer runs, the store does change to the new value, but the redux devtools shows that the store was always at the new value and never the old value. Am I mutating state in the editSnippetReducerFunction reducer or something?
const addSnippetReducerFunction = (state: any, action): any => {
return Object.assign({}, state, {
snippets: [
...state.snippets,
{
text: action.payload.text,
id: action.payload.id
}
]
})
}
const editSnippetReducerFunction = (state: any, action): any => {
const newSnippets = state.snippets.map(snippet => {
if (snippet.id === action.payload.id) {
snippet.text = action.payload.text
return snippet
} else {
return snippet
}
})
return { snippets: newSnippets, ...state}
}
const removeSnippetReducerFunction = (state: any, action): any => {
const newSnippets = state.snippets.filter(snippet => snippet.id !== action.payload.id)
return { snippets: newSnippets, history: [] }
}
export const rootReducer: any = createReducer(initialState, {
ADD_SNIPPET: addSnippetReducerFunction,
EDIT_SNIPPET: editSnippetReducerFunction,
REMOVE_SNIPPET: removeSnippetReducerFunction
})
The action is dispatched with the correct details. It is only the editSnippetReducerFunction reducer function that has this issue, The other reducers shown above do work correctly.
EDIT: It actually works if I stop using react-redux connect on the component and I move the action to the parent component which is connected and working.
The component that doesn't work when connected:
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { withStyles } from '#material-ui/core/styles'
import ListItem from '#material-ui/core/ListItem'
import ListItemIcon from '#material-ui/core/ListItemIcon'
import ListItemText from '#material-ui/core/ListItemText'
import ListItemSecondaryAction from '#material-ui/core/ListItemSecondaryAction'
import Button from '#material-ui/core/Button'
import MoreHorizIcon from '#material-ui/icons/MoreHoriz'
import CodeIcon from '#material-ui/icons/Code'
import { styles } from './snippet.style.js'
import Menu from '#material-ui/core/Menu'
import MenuItem from '#material-ui/core/MenuItem'
import { removeSnippet } from '../app/action'
import { bindActionCreators } from 'redux'
import type { Dispatch } from 'redux'
import { connect } from 'react-redux'
const mapDispatchToProps = (dispatch: Dispatch): any => {
return bindActionCreators(
{
removeSnippet: removeSnippet
},
dispatch
)
}
class SnippetComponent extends Component<any, any> {
constructor(props) {
super(props)
this.state = {
anchorEl: undefined
}
}
handleClick = event => {
this.setState({ anchorEl: event.currentTarget })
}
handleClose = () => {
this.setState({ anchorEl: null })
}
handleRemove = () => {
this.props.removeSnippet({snippetId: this.props.snippet.id})
}
render = () => {
return (
<ListItem
button
onClick={() => this.props.onSnippetClick(this.props.snippet)}
className={this.props.classes.listItem}>
<ListItemIcon>
<CodeIcon />
</ListItemIcon>
<ListItemText
style={{
marginRight: '100px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
primary={this.props.snippet.text}
/>
<ListItemSecondaryAction>
<Button
variant="fab"
color="primary"
aria-owns={this.state.anchorEl ? 'simple-menu' : null}
aria-haspopup="true"
onClick={this.handleClick}
className={this.props.classes.iconHover}
style={{
marginRight: '50px',
boxShadow: 'none',
color: 'white'
}}
aria-label="Add">
<MoreHorizIcon />
</Button>
<Menu
id="simple-menu"
anchorEl={this.state.anchorEl}
open={Boolean(this.state.anchorEl)}
onClose={this.handleClose}>
<MenuItem onClick={this.handleRemove}>Remove Snippet</MenuItem>
<MenuItem onClick={this.handleClose}>Share</MenuItem>
</Menu>
</ListItemSecondaryAction>
</ListItem>
)
}
}
SnippetComponent.propTypes = {
classes: PropTypes.object.isRequired,
snippet: PropTypes.object.isRequired,
addToCart: PropTypes.func.isRequired
}
const Snippet = withStyles(styles)(
connect(
undefined,
mapDispatchToProps
)(SnippetComponent)
)
export default withStyles(styles)(Snippet)
The parent component:
import React, { Component } from 'react'
import './App.css'
import brace from 'brace'
import AceEditor from 'react-ace'
import 'brace/mode/javascript'
import 'brace/theme/gruvbox'
import Button from '#material-ui/core/Button'
import AddIcon from '#material-ui/icons/Add'
import { bindActionCreators, createStore } from 'redux'
import type { Dispatch } from 'redux'
import { addSnippet, editSnippet, removeSnippet } from './action'
import { connect, Provider } from 'react-redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import PropTypes from 'prop-types'
import Grid from '#material-ui/core/Grid'
import Snippet from '../snippet/Snippet'
import List from '#material-ui/core/List'
import { rootReducer } from './app.reducer.js'
import type { Props, AppState } from './app.model.js'
import { appHeaderHeight } from './app.style.js'
import { withStyles } from '#material-ui/core/styles'
import { styles } from './app.style.js'
import Header from '../header/Header'
import ConfirmDialog from '../confirm/ConfirmDialog'
// and so is this. proptypes needs it in initial state and also mapstatetoprops
const mapStateToProps = (
state: any,
ownProps: { buttonColour: string }
): any => ({
snippets: state.snippets,
history: state.history,
buttonColour: ownProps.buttonColour
})
const mapDispatchToProps = (dispatch: Dispatch): any => {
return bindActionCreators(
{
addSnippet: addSnippet,
editSnippet: editSnippet
},
dispatch
)
}
class AppComponent extends Component<Props, AppState> {
constructor(props) {
super(props)
this.state = {
width: 0,
height: 0,
editor: React.createRef(),
saveButtonDisabled: true,
editorValue: '',
open: false,
lastClickedSnippet: ''
}
}
componentDidMount = () => {
this.updateWindowDimensions()
window.addEventListener('resize', this.updateWindowDimensions)
}
componentWillUnmount = () => {
window.removeEventListener('resize', this.updateWindowDimensions)
}
updateWindowDimensions = () => {
this.setState({
width: window.innerWidth,
height: window.innerHeight
})
}
onEditorChange = editorValue => {
if (editorValue.length > 5) {
this.setState({ saveButtonDisabled: false })
} else {
this.setState({ saveButtonDisabled: true })
}
this.setState({ editorValue: editorValue })
}
onSaveButtonClick = () => {
this.setState({ saveButtonDisabled: true })
if (this.state.lastClickedSnippet) {
this.props.editSnippet({
snippetId: this.state.lastClickedSnippet.id,
snippetText: this.state.editorValue
})
this.setState({ lastClickedSnippet: undefined })
} else {
this.props.addSnippet({
text: this.state.editor.current.editor.getValue()
})
}
this.setState({ editorValue: '' })
}
onSnippetClick = (snippet: Snippet) => {
this.setState({ lastClickedSnippet: snippet })
this.setState({ open: true })
}
onDialogClose = value => {
this.setState({ value, open: false })
}
handleOk = () => {
this.setState({ editorValue: this.state.lastClickedSnippet.text })
this.onDialogClose(this.state.value)
};
handleCancel = () => {
this.setState({ lastClickedSnippet: undefined })
this.onDialogClose(this.state.value)
};
render = () => {
let allSnippets = []
if (this.props.snippets) {
allSnippets = this.props.snippets.map(snippet => (
<Snippet
snippet={snippet}
key={snippet.id}
onSnippetClick={this.onSnippetClick}
editSnippet={this.props.editSnippet}
/>
))
}
return (
<div className="App">
<ConfirmDialog
handleOk={this.handleOk}
handleCancel={this.handleCancel}
open={this.state.open}
onDialogClose={this.onDialogClose}
value={this.state.value}
/>
<Header />
<div
className={this.props.classes.bodyContainer}
style={{ height: this.state.height - appHeaderHeight - 70 }}>
<Grid
container
spacing={0}
alignItems={'flex-start'}
direction={'row'}
justify={'flex-start'}>
<Grid
item
sm={12}
md={6}
className={this.props.classes.leftGrid}
style={{ height: this.state.height - appHeaderHeight - 70 }}>
<Button
className={this.props.classes.saveButton}
variant="fab"
color="secondary"
aria-label="Add"
disabled={this.state.saveButtonDisabled}
onClick={this.onSaveButtonClick}>
<AddIcon />
</Button>
<AceEditor
mode="javascript"
theme="gruvbox"
width="100%"
value={this.state.editorValue}
onChange={this.onEditorChange}
height={this.state.height - appHeaderHeight - 70}
name="editor"
editorProps={{ $blockScrolling: true }}
ref={this.state.editor}
/>
</Grid>
<Grid
item
sm={12}
md={6}
className={this.props.classes.rightGrid}
style={{ height: this.state.height - appHeaderHeight - 70 }}>
<List component="nav" className={this.props.classes.navList} />
{allSnippets}
</Grid>
</Grid>
</div>
</div>
)
}
}
const App = withStyles(styles)(
connect(
mapStateToProps,
mapDispatchToProps
)(AppComponent)
)
AppComponent.propTypes = {
snippets: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
text: PropTypes.string.isRequired
}).isRequired
).isRequired,
history: PropTypes.array,
addSnippet: PropTypes.func.isRequired
}
const store = createStore(rootReducer, composeWithDevTools())
const SnippetApp = () => (
<Provider store={store}>
<App />
</Provider>
)
export default SnippetApp
Am I mutating state in the editSnippetReducerFunction reducer or something?
Yes, you are. You are mutating the snipped that you want to update.
You probably want to do this instead:
const editSnippetReducerFunction = (state: any, action): any => ({
...state,
snippets: state.snippets.map(snippet =>
snippet.id === action.payload.id
? {...snipped, text: action.payload.text}
: snipped
)),
});
Also, notice that this:
return {snippets: newSnippets, ...state}
Is not the same as this:
return {...state, snippets: newSnippets}
With first one, if state has a property named snippets that property will be used, instead of your newSnippets. With the second one, on the other hand, the snippets property will be updated with the newSnippets.
Basically, the first one is sugar for:
return Object.assign({}, {snippets: newSnippets}, state);
While the second one is the equivalent of:
return Object.assign({}, state, {snippets: newSnippets});
In any event, the implementation of editSnippetReducerFunction that I'm suggesting addresses the 2 different problems that you had with your original implementation.