Simple state managment with contextAPI in NextJS - javascript

I want to set up a simple global state so that the site owner can update some numeric values whenever he wants to. I created a Provider, added it into custom _app.js and into his admin.js where he can change the value via input and into aboutme.js in which the value should get displayed at the end.
Here is my StateProvider.js:
import React, { useContext, createContext, useState } from 'react';
export const AppContext = createContext();
export const StateProvider = ({ children }) => {
const [dvds, setDvds] = useState(Number);
let sharedState = {
dvds,
setDvds,
};
return (
<AppContext.Provider value={sharedState}>{children}</AppContext.Provider>
);
};
export function useAppContext() {
return useContext(AppContext);
}
which I imported into _app.js:
import '../styles/globals.scss';
import { StateProvider } from '../lib/appCtx';
function MyApp({ Component, pageProps }) {
return (
<>
<StateProvider>
<Component {...pageProps} />
</StateProvider>
</>
);
}
}
export default MyApp;
Here is the admin.js to set the value:
import React, { useEffect, useState, useContext, createContext } from 'react';
import styles from '../styles/Admin.module.scss';
import { AppContext } from '../lib/appCtx';
export default function Admin() {
const { dvds, setDvds } = useContext(AppContext);
const updateState = () => createContext(AppContext);
return (
<>
...
<div className={styles.card}>
<div className={styles.title}>Number of DVDs</div>
<p className={styles.text}>
{new Intl.NumberFormat('de-DE', {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(dvds)}
</p>
<input
type='text'
onChange={(e) => setDvds(e.target.value)}
onKeyPress={updateState}
/>
</div>
</div>
...
</>
);
}
and last but not least the aboutme.js to display the value from context:
import Image from 'next/image';
import styles from '../styles/Aboutme.module.scss';
import { AppContext, useAppContext } from '../lib/appCtx';
export default function AboutMe() {
function thousands_separators(num) {
var num_parts = num.toString().split('.');
num_parts[0] = num_parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return num_parts.join('.');
}
const dvds = AppContext.dvs;
return (
<div>
...
<div className={styles.col}>
<p>
<span>
{new Intl.NumberFormat('de-DE', {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(useAppContext(dvds))}
</span>
<br /> dvds in collection
</p>
</div>
</div>
...
);
}
So I want to pass the typed number from admin.js over the Provider into the aboutme.js.
Would anyone be so nice to explain me why it doesn`t work and helping me by providing the right source? Thank you guys!!

Related

How to pass context to _document.js in Next.js?

I want to change the thumb color of the scroll bar depending on the page I am on. Currently, I am using this script that I had made:
// /scripts/scrollbar.js
export default function updateScrollbar(thumbColor) {
const html = document.getElementsByTagName("html")[0];
html.style.setProperty("--thumb", thumbColor);
}
And if I am on some page, I have the following code to update the scroll bar thumb color
// /pages/Comp.js
import { useEffect } from "react";
import updateScrollbar from "../../scripts/scrollbar";
export default function Comp() {
useEffect(() => {
updateScrollbar("var(--purple)");
}, []);
return (
<p>hi</p>
);
}
This works fine. I visit a page and the scrollbar color updates.
I don't want to use to this scrollbar.js script though. So I tried the following:
// /Layouts/Layout.js
import { createContext, useState } from "react";
export const AppContext = createContext();
export default function Layout({ children }) {
const [thumbColor, setThumbColor] = useState("var(--white)");
const context = { thumbColor, setThumbColor };
return (
<AppContext.Provider value={context}>
{ children }
</AppContext.Provider>
)
}
I have created a context and passed thumbColor and setThumbColor as Provider's value.
// /pages/_app.js
import Layout from '../Layouts/Layout';
import '../styles/globals.css';
function MyApp({ Component, pageProps }) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
)
}
export default MyApp;
The layout wraps the _app.js.
// /pages/Comp.js
import { useContext, useEffect } from "react";
import { AppContext } from "../../Layouts/Layout";
export default function Comp() {
const { setThumbColor } = useContext(AppContext);
useEffect(() => {
setThumbColor("var(--purple)");
}, []);
return (
<p>hi</p>
);
}
I get the set function from AppContext and want to update the thumbColor state from here.
import Document, { Html, Head, Main, NextScript } from "next/document";
import { AppContext } from "../Layouts/Layout";
class MyDocument extends Document {
static contextType = AppContext;
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx);
return { ...initialProps }
}
render() {
let temp = this.context; // this is undefined
return (
<Html style={{ "--thumb": temp.thumbColor }} lang="en">
<Head>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument;
Since context is undefined, I am not able to do anything. What am I doing wrong ?
Thanks for helping !

React Context api, I can't get to object from context

I started to learn React, I'm trying to retrieve data from api, the data is an object with the fields of base, date & rates, without any problem I can print and logout base & date but rates which is an object not.
console.log gives undefined, when trying to iterate is obviously that the object does not exist but in DevTools i can see normal data
Thank you for your help and greetings
Context:
export const ExchangeProvider = props => {
const [lastestExchanges, setLastestExchanges] = useState({})
const fetchLastestExchange = async () => {
try {
await fetch(`https://api.exchangeratesapi.io/latest`).then(data => data.json()).then(data => setLastestExchanges(data))
} catch (err) {
console.log(err)
}
}
useEffect(() => {
fetchLastestExchange()
}, [])
return (
<ExchangeContext.Provider value={[lastestExchanges, setLastestExchanges]}>
{props.children}
</ExchangeContext.Provider>
)
}
Usage:
import React, {useState, useContext} from "react";
import {ExchangeContext} from "../ExchangeContext";
function HomeView() {
const [lastestExchange, setLastestExchange] = useContext(ExchangeContext)
console.log(lastestExchange)
return (
<div className="container">
<p>{lastestExchange.base}</p>
<p>{lastestExchange.date}</p>
{/*<p>{lastestExchange.rates['PLN']}</p>*/}
<ul>
{/*{Object.keys(lastestExchange.rates).map(key => <li>{lastestExchange.rates[key]}</li>)}*/}
</ul>
</div>
)
}
export default HomeView
Provider usage:
import React from 'react';
import HomeView from "./Views/HomeView";
import {
BrowserRouter as Router,
Switch,
Route,
Link
} from "react-router-dom";
import {ExchangeProvider} from "./ExchangeContext";
function App() {
return (
<ExchangeProvider>
<div className="App container w-full flex h-full">
<Router>
<Switch>
<Route path="/">
<HomeView/>
</Route>
</Switch>
</Router>
</div>
</ExchangeProvider>
);
}
export default App;
You can use react context simpler like this :
// src/ThemeContext.js
import React from 'react';
const ThemeContext = React.createContext(null);
export default ThemeContext;
// src/ComponentA.js
import React from 'react';
import ThemeContext from './ThemeContext';
const A = () => (
<ThemeContext.Provider value="green">
<D />
</ThemeContext.Provider>
);
// src/ComponentD.js
import React from 'react';
import ThemeContext from './ThemeContext';
const D = () => (
<ThemeContext.Consumer>
{value => (
<p style={{ color: value }}>
Hello World
</p>
)}
</ThemeContext.Consumer>
);

React.js: How to implement dark/light mode in body toggling with useContext?

I am trying to create a background theme which will switch on onClick. On onClick it must change the background color of body in react app. I've managed to implement useContext, and now it toggles and changes the list items color in Header component. How to set it to body as well? Any help will be appreciated.
Here is my useContext color component
import React from 'react'
export const themes = {
light: {
foreground: '#ffffff',
},
blue: {
foreground: 'blue',
},
}
export default React.createContext({
theme: themes.light,
switchTheme: () => {},
})
onClick Button component
import React, { useContext } from 'react'
import ThemeContext from './context'
import './ThemedButton.scss'
const ThemedButton = () => {
const { switchTheme } = useContext(ThemeContext)
return (
<>
<button className="btn" onClick={switchTheme}>
Switch
</button>
</>
)
}
export default ThemedButton
App.js
import React, { useState } from 'react'
import SearchBar from './components/SearchBar';
import useCountries from './Hooks/useCountries';
import MainTable from './components/MainTable';
import ThemeButton from './useContext/ThemedButton';
import ThemeContext from './useContext/context';
import { searchProps } from './types';
import { themes } from './useContext/context';
import Routes from './Routes';
import './App.scss'
export default function App() {
const [search, setSearch] = useState('')
const [data] = useCountries(search)
const [context, setContext] = useState({
theme: themes.light,
switchTheme: () => {
setContext((current) => ({
...current,
theme: current.theme === themes.light ? themes.blue : themes.light,
}))
},
})
const handleChange: React.ReactEventHandler<HTMLInputElement> = (e): void => {
setSearch(e.currentTarget.value)
}
return (
<div className="App">
<SearchBar handleChange={handleChange} search={search as searchProps} />
<ThemeContext.Provider value={context}>
<ThemeButton />
<MainTable countries={data} />
</ThemeContext.Provider>
<Routes />
</div>
)
}
Header component
import React, { useContext } from 'react'
import ThemeContext from '../../useContext/context'
import './Header.scss'
export default function Header() {
const { theme } = useContext(ThemeContext)
return (
<div className="header">
<ul className="HeadtableRow" style={{ color: theme.foreground }}> // here it's set to change list items color
<li>Flag</li>
<li>Name</li>
<li>Language</li>
<li>Population</li>
<li>Region</li>
</ul>
</div>
)
}
If you want to change your body tag in your application you need to modify DOM and you can add this code to your Header.js (or any other file under your context) file:
useEffect(() => {
const body = document.getElementsByTagName("body");
body[0].style.backgroundColor = theme.foreground
},[])
*** Don't forget to import useEffect
*** Inline style like below is a better approach than modifying DOM directly
<div className="App" style={{backgroundColor: context.theme.foreground}}>
//For under context files just use theme.foreground
<SearchBar handleChange={handleChange} search={search as searchProps} />
<ThemeContext.Provider value={context}>
<ThemeButton />
<MainTable countries={data} />
</ThemeContext.Provider>
<Routes />
</div>

my react code is working but when i refresh the page i get TypeError: Cannot read property 'Location' of undefined

Starting with GamePage, it provides 2 routes which renders the components GameList and GameDetailPage. Both work fine at first but When i refresh the page for Gamelist component, it still rerenders the page but when i refresh the page for GameDetailPage, i get the error TypeError: Cannot read property 'Location' of undefined. I do not understand why it is unable to fetch data from state whenever i refresh.
gamepage.jsx
import React from "react";
import GamesList from "../../components/games-list/game-list.component";
import { Route } from "react-router-dom";
import GameDetailPage from "../gamedetailpage/gamedetailpage.component";
import {firestore,convertCollectionsSnapshotToMap} from '../../firebase/firebase.utils'
import {connect} from 'react-redux'
import {updateFootballGames} from '../../redux/games/games.actions'
class GamePage extends React.Component {
unsubscribeFromSnapshot=null;
//whenever the component mounts the state will be updated with the football games.
componentDidMount(){
const {updateFootballGames}=this.props
const gameRef=firestore.collection('footballgames')
gameRef.onSnapshot(async snapshot=>{
const collectionsMap=convertCollectionsSnapshotToMap(snapshot)
updateFootballGames(collectionsMap)
})
}
render() {
const { match } = this.props;
return (
<div className="game-page">
<h1>games page</h1>
<Route exact path={`${match.path}`} component={GamesList} />
<Route path={`${match.path}/:linkUrl`} component={GameDetailPage}
/>
</div>
);
}
}
const mapStateToProps=state=>({
games:state.games.games
})
const mapDispatchToProps=dispatch=>({
updateFootballGames:collectionsMap=>
dispatch(updateFootballGames(collectionsMap))
})
export default connect(mapStateToProps, mapDispatchToProps)(GamePage);
gamedetailpage.component.jsx
import React from "react";
import { connect } from "react-redux";
import GamePreview from '../../components/game-preview/game-preview.component'
import GameDetails from '../../components/game-details/game-details.component'
const GameDetailPage = (props) => {
const {games, match} = props
const urlparam =match.params.linkUrl
// const games_array = Object.entries(games)
const gameObj=games[urlparam]
console.log('prop',gameObj)
return (
<div className="game-list">
<GameDetails game = {gameObj}/>
</div>
);
};
const mapStateToProps = (state) => ({
games: state.games.games,
});
export default connect(mapStateToProps)(GameDetailPage);
game_details.component.jsx
import React from 'react';
const GameDetails = (props) => {
console.log(props.game.Location)
return(
<div>
Location:{props.game.Location}
<br/>
Price:{props.game.Price}
</div>
)
}
export default GameDetails;
gamelist.component.jsx
import React from "react";
import './game-list.styles.scss'
import GamePreview from "../game-preview/game-preview.component";
import {connect} from 'react-redux'
const GameList=(props)=>{
const {games}=props
console.log(games)
const game_list=Object.entries(games)
console.log(game_list)
return (
<div className="game-list">
{game_list.map(game =>
<GamePreview game = {game[1]}/>)}
</div>
);
}
const mapStateToProps=state=>({
games:state.games.games
})
export default connect(mapStateToProps)(GameList);
gamepreview.component.jsx
import React from "react";
import "./game-preview.styles.scss";
import { withRouter, Route } from "react-router-dom";
import GamePreviewDetail from "../game-preview-detail/game-preview-detail.component";
const GamePreview = (props) => {
const { Location, Time, linkUrl, Price } = props.game;
const { history, match } = props;
return (
<div
className="game-preview"
onClick={() => history.push(`${match.url}/${linkUrl}`)}
>
<div className="game-preview-image">
<p>Picture goes here</p>
</div>
{/* <GamePreviewDetail name = {Location} price={Price}/> */}
<p>Location:{Location}</p>
<p>Price:{Price}</p>
</div>
);
};
export default withRouter(GamePreview);
app.js
import React from 'react';
import './App.css';
//import dependencies
import { Route, Switch } from "react-router-dom";
//import pages
import HomePage from './pages/homepage/homepage'
import GamesPage from './pages/gamespage/gamespage'
import SignInSignUp from './pages/signin-signup-page/signin-signup-page'
import GameDetailPage from "./pages/gamedetailpage/gamedetailpage.component";
import Header from './components/header/header.component';
import { auth, createUserProfileDocument } from './firebase/firebase.utils';
class App extends React.Component{
constructor() {
super();
this.state = {
currentUser: null
}
}
unsubscribeFromAuth = null
componentDidMount() {
this.unsubscribeFromAuth = auth.onAuthStateChanged(async userAuth => {
if (userAuth) {
const userRef = await createUserProfileDocument(userAuth);
// check if the snapshot has changed (subscribe)
// get the user that we just created or that already exists in the db
userRef.onSnapshot(snapshot => {
this.setState({
currentUser: {
id: snapshot.id,
...snapshot.data()}
})
})
} else {
this.setState({currentUser: userAuth})
}
})
}
componentWillUnmount() {
this.unsubscribeFromAuth();
}
render(){
return(
<div>
<Header currentUser = {this.state.currentUser}/>
<Switch>
<Route exact path="/" component={HomePage} />
<Route path="/games" component={GamesPage} />
<Route exact path="/signin" component={SignInSignUp} />
</Switch>
</div>
)
}
}
export default App;
I would try using useParams hook instead. Then capturing any changes of linkUrl with useEffect hook. Also introducing gameObj with useState.
useParams returns an object of key/value pairs of URL parameters. Use it to access match.params of the current <Route>.
If you're familiar with React class lifecycle methods, you can think of useEffect Hook as componentDidMount, componentDidUpdate, and componentWillUnmount combined.
Try to modify <GameDetailPage /> component as the following:
import React, { useState, useEffect } from 'react';
import { useParams } from "react-router-dom";
// other imports
const GameDetailPage = (props) => {
const { games } = props;
let { linkUrl } = useParams();
const [ gameObj, setGameObj ] = useState(null);
useEffect(() => {
if (games) {
const newGameObj = games[linkUrl];
console.log('game object', newGameObj);
setGameObj(newGameObj);
}
}, [games, linkUrl]);
return <div className="game-list">
{ gameObj && <GameDetails game={ gameObj } /> }
</div>
}
+1 - null check:
Also you can see a null check in the return statement for gameObj which helps rendering only that case once you have a value in games array with found linkUrl value.
I hope this helps!

React Hook to find user

I am trying to use the React useState Hook for an online project. What I want to happen is, when I type the users name in the search box, It will find the users card on the browser. I am able to log the user to the console, but I am stuck on how to get it to render on screen. Tried so many ways and just not getting it.
console output
App.js
import React, { useState } from 'react';
import CardList from './CardList';
import {robots} from './robots';
import SearchBox from './SearchBox';
function App() {
let [searchInput] = useState('');
function onSearchChange(e) {
searchInput = e.target.value;
const filteredRobots = robots.filter(function(robot){
return robot.name.toLowerCase().includes(searchInput.toLowerCase());
});
console.log(filteredRobots);
}
return (
<div className='tc'>
<h1>RoboFriends</h1>
<SearchBox searchChange={onSearchChange} />
<CardList id={robots.id} name={robots.name} email={robots.email}/>
</div>
);
}
export default App;
CardList.js
import React from 'react';
import Card from './Card';
import {robots} from './robots';
function CardList(props) {
return (
<div>
{
robots.map(function(user) {
return <Card key={user.id} id={user.id} name={user.name} email={user.email} />
})
};
</div> )
}
export default CardList;
Card.js
import React from 'react';
import 'tachyons';
function Card(props) {
return (
<div className='bg-light-green dib br3 pa3 ma2 grow shadow-5'>
<img src={`https://robohash.org/${props.id}`} alt="Robot" />
<h2>{props.name}</h2>
<p>{props.email}</p>
</div>
);
}
export default Card;
React only re-render when you set a state to a new value.
Check the code below:
import React, { useState } from 'react';
import CardList from './CardList';
import {robots} from './robots';
import SearchBox from './SearchBox';
function App() {
let [searchInput, setSeachInput] = useState('');
function onSearchChange(e) {
// set state here to re-render
setSeachInput(e.target.value);
}
// use might want to use useMemo to improve this, I just want to make it simple now
const filteredRobots = robots.filter(function(robot){
return robot.name.toLowerCase().includes(searchInput.toLowerCase());
});
console.log(filteredRobots);
return (
<div className='tc'>
<h1>RoboFriends</h1>
<SearchBox searchChange={onSearchChange} />
{/* using filteredRobots herer*/}
<CardList id={filteredRobots.id} name={filteredRobots.name} email={filteredRobots.email}/>
</div>
);
}
export default App;
In your App.js file, the searchInput is not being set to the state
import React, { useState } from 'react';
import CardList from './CardList';
import {robots} from './robots';
import SearchBox from './SearchBox';
function App() {
let [searchInput, setSearchInput] = useState('');
function onSearchChange(e) {
setSearchInput(e.target.value)
}
**You can pass the filterRobots in place of robots to get only words passed in the search box**
const filteredRobots = robots.filter(function(robot){
return robot.name.toLowerCase().includes(searchInput.toLowerCase());
});
return (
<div className='tc'>
<h1>RoboFriends</h1>
<SearchBox searchChange={onSearchChange} />
<CardList robots={filteredRobots}/>
</div>
);
}
export default App;
In the CardList File
import React from 'react';
import Card from './Card';
function CardList({robots}) {
{
robots.map((user, i) => {
return (
<Card
key={i}
id={user[i].id}
name={user[i].name}
email={user[i].email}
/>
);
})
}
}
export default CardList;
You should not be mutating the searchInput value like searchInput = e.target.value. It is better to call a setter function to update the value. For example,
const [searchInput, setSearchInput] = useState('');
// to update the value of searchInput call setSearchInput
function onSearchChange(e) {
setSearchInput(e.target.value)
}
State changes are asynchronous. When you try to filter the robots it is not guaranteed that it will be called with the latest value of searchInput that's why you should be using useEffect hook which will filter the robots when the value of searchInput changes.
Here is a solution,
import React, { useState } from 'react';
import CardList from './CardList';
import {robots} from './robots';
import SearchBox from './SearchBox';
function App() {
let [searchInput, setSearchInput] = useState('');
let [filterdRobots, setFilteredRobots] = useState(robots);
function onSearchChange(e) {
setSearchInput(e.target.value);
}
useEffect(() => {
setFilteredRobots(robots.filter(r =>
r.name.toLowerCase().includes(searchInput.toLowerCase())))
}, [searchInput, robots])
return (
<div className='tc'>
<h1>RoboFriends</h1>
<SearchBox searchChange={onSearchChange} />
<CardList robots={filteredRobots}/>
</div>
);
}
export default App;
check the codesanbox for demo

Categories