I've switched from nesting props to my components into React's Context API. I've created a class to serve me some desired methods:
export default class StepDatabase {
private readonly steps: Steps = steps;
private currentStep: number = steps[0].step;
public getStep(): Step {
return this.steps[this.currentStep];
}
public nextStep(): void {
if (this.currentStep === this.steps.length) return;
this.currentStep++;
}
}
And then, created a context:
const stepsInstance = new StepsDatabase();
export const StepsContext = createContext<StepsDatabase>(stepsInstance);
Of course, then provided it:
const App = () => (
<div className={styles.App_container}>
<main className={styles.App_grid}>
<StepsContext.Provider value={stepsInstance}>
<Sidebar />
<Content />
</StepsContext.Provider>
</main>
</div>
);
And tried using it in my Sidebar component:
const Sidebar = () => {
const StepContext = React.useContext(StepsContext);
const currentStep = StepContext.getStep();
return (
<section className={`${styles.App_gridItem} ${styles.App_sideItem}`}>
<SidebarHeading>{currentStep.stepName}</SidebarHeading>
<SidebarParagraph>{currentStep.stepDescription}</SidebarParagraph>
<button onClick={() => StepContext.nextStep()}>step</button>
</section>
);
};
But the SidebarHeading and SidebarParagraph wasn't updating at all after clicking my button. The first step has worked fine. What's the problem?
There is nothing in your code that triggers the context to re-render. If the context does not re-render, it won't be able to trigger all of the components that consume it. You need something at a higher level to cause the context to re-render, or you need to pass a function within your context to your consumers that may trigger the re-render. See the documentation.
Here is an example based upon your code:
import React, { createContext, useState } from "react";
import "./styles.css";
const StepsContext = createContext();
const Sidebar = () => {
const { step, setNextStep } = React.useContext(StepsContext);
return (
<section>
<div>Heading: {step.stepName}</div>
<div>Paragraph: {step.stepDescription}</div>
<button onClick={() => setNextStep()}>step</button>
</section>
);
};
export default function App() {
const [steps, setSteps] = useState([
{ stepName: "Step 1", stepDescription: "My description 1" },
{ stepName: "Step 2", stepDescription: "My description 2" }
]);
const [currentStep, setCurrentStep] = useState(0);
return (
<div>
<main>
<StepsContext.Provider
value={{
step: steps[currentStep],
setNextStep: function () {
if (currentStep < steps.length - 1) {
setCurrentStep(currentStep + 1);
}
}
}}
>
<Sidebar />
<div>Content</div>
</StepsContext.Provider>
</main>
</div>
);
}
Related
I'm trying to understand React's compound pattern. In all exercises all components are in one file. Is it possible to build component with that pattern with external components?
I would achieve that scenario:
src:
components:
Main
Component1
Component2
Component3
// ONE FILE Main.js
import {CompoundComponent1, CompoundComponent2, CompoundComponent3} './foo'
const Main = () => {
const [on, setOn] = React.useState(false)
const toggle = () => setOn(!on)
const CompoundComponent1 = Component1;
const CompoundComponent2 = Component2;
const CompoundComponent3 = Component3;
return <Switch on={on} onClick={toggle} />
}
Main.C1 = CompoundComponent1
Main.C2 = CompoundComponent2
Main.C3 = CompoundComponent3
// ONE FILE END
App.js
const App = () => {
<Main>
<Main.C1>FOO</Main.C1>
// etc.
</Main>
}
I think that i found solution.
import * as React from 'react'
import {Switch} from '../switch'
import {ToggleOn} from './02/ToggleOn'
import {ToggleOff} from './02/ToggleOff'
import {ToggleButton} from './02/ToggleButton'
function Toggle() {
const [on, setOn] = React.useState(false)
const toggle = () => setOn(!on)
return <Switch on={on} onClick={toggle} />
}
Toggle.ToggleOn = ToggleOn
Toggle.ToggleOff = ToggleOff
Toggle.ToggleButton = ToggleButton
function App() {
return (
<div>
<Toggle>
<Toggle.ToggleOn>Turn ON</Toggle.ToggleOn>
<Toggle.ToggleOff>Turn OFF</Toggle.ToggleOff>
<Toggle.ToggleButton />
</Toggle>
</div>
)
}
export default App
In separated files:
export const ToggleButton = ({on, toggle}) => (
<Switch on={on} onClick={toggle} />
)
export const ToggleOn = ({on, children}) => {
if (on) {
return children
}
return null
}
I was implementing an e-commerce cart using react and inside it, I created a single context to get the category of the item user wants to see. I also used the react-router-dom to route a new component to display all the elements using the state of the context. But although I updated the state, it is showing that my state is empty.
function ProductCategory() {
const [categoryClick, setCategoryClick] = useState('LAPTOPS');
const {product, activeCategoryChanged} = useActiveCategoryContext();
const handleCategoryClick = (e) => {
setCategoryClick(e.target.id);
activeCategoryChanged(obj[categoryClick]);
}
const getProductCategoriesCard = () => {
return <ProductCategoryCardWrapper onClick={handleCategoryClick}>
{
PRODUCT_CATEGORIES.map((category, index) => {
return <ProductCards id = {category.name} key={index} style={{ backgroundImage : "url("+category.imageURL+")"}}><Heading id = {category.name}><Link to ='/ecom/category'>{category.name}</Link></Heading></ProductCards>
})
}
</ProductCategoryCardWrapper>
}
return (
<section>
{getProductCategoriesCard()}
{/* <Products product = {obj[categoryClick]}/> */}
</section>
)
}
Now below is the code sample of the context:
import React, {useContext, useState, useEffect} from 'react';
const ActiveCategoryContext = React.createContext();
export function useActiveCategoryContext() {
return useContext(ActiveCategoryContext);
}
export function ActiveCategoryContextProvider({children}){
const [product, setProduct] = useState([]);
const activeCategoryChanged = (active) => {
setProduct(active);
}
const value = {
product,
setProduct,
activeCategoryChanged
}
return <ActiveCategoryContext.Provider value = {value}>
{children}
</ActiveCategoryContext.Provider>
}
Now finally I am going to attach the code sample of the product component which uses the product state of the context to display all the elements inside the particular category selected by the user:
function Products() {
const {product} = useActiveCategoryContext();
console.log(product);
const getProductItemsDisplayed = () => {
return product.map((product, index) => (
<ProductCartCard key={index} product={product} />
));
};
return <TopSection>
{getProductItemsDisplayed()}
</TopSection>;
}
I have requirement where I need to load component from dynamic folders. For example I have following folders inside components
components
-default
-component-one
-component-two
-component-three
-custom
-component-three
Suppose if componentFolder state set to custom folder then it should load from custom folder .if any component not found in custom folder then it should be load from default folder.
So my question is ,can we possible to import recursively ?
function App() {
const [componentFolder, setComponentFolder] = React.useState("default")
const Home = React.lazy(() => import("./components/" +componentFolder+ "/Home"));
return (
<div className="App">
<Suspense fallback="laoding">
<Home></Home>
</Suspense>
</div>
);
}
the below link has same requirement as i asked
How to check if a pariticular fileExists in reactjs
If you are using Webpack then you can use require.context to load modules dynamically:
import React, { Suspense } from "react";
const load = async (path, file) => {
const defaultPath = "default";
const files = require.context("./components", true, /\.js$/);
try {
return files(`./${path}/${file}.js`);
} catch (err) {
return files(`./${defaultPath}/${file}.js`);
}
};
export default function App() {
const [componentFolder, setComponentFolder] = React.useState("default");
const Home = React.lazy(() => load(componentFolder, "Home"));
return (
<div className="App">
<Suspense fallback="loading">
<Home />
</Suspense>
</div>
);
}
Since lazy returns a promise, you can use its catch block to return another lazy (promise) when the original module was not found.
An example:
import { lazy, Suspense, useState } from "react";
const rotate = {
custom: "default",
default: "custom",
};
function App() {
const [folder, setFolder] = useState("custom");
const [name, setName] = useState("component1");
// Here: catch and return another lazy (promise)
const Component = lazy(() =>
import("./components/" + folder + "/" + name).catch(
(err) => import("./components/" + rotate[folder] + "/" + name)
)
);
return (
<div>
<Suspense fallback="laoding">
<Component />
</Suspense>
<button onClick={() => setFolder(rotate[folder])}>toggle folder</button>
<br />
<button onClick={() => setName("component1")}>load component 1</button>
<button onClick={() => setName("component2")}>load component 2</button>
<button onClick={() => setName("component3")}>load component 3</button>
</div>
);
}
Here is a demo.
Note that Component, defined/created inside App component, will be recreated at every rerender of App. It will cause Component to reset its state when App rerenders.
Based in the others answers and comments here I came up with this:
https://codesandbox.io/s/so-react-lazy-recursive-import-2dqlp?file=/src/App.js
import React, { lazy, Suspense } from "react";
// test the code by removing the _ in front of file names
/*
components/
comp 🚫 3? a root file will not trigger -> go to default
/default
comp 👈 3! nice ^^ (but if it not exists will throw an error)
/custom
comp 👈 2?
/client
comp 👈 1?
/omgStop
heIsAlreadyDead (but works)
/otherClient ...
*/
const recursiveImport = async (
componentName,
targetTree,
defaultTree = "./components/default"
) => {
console.count("paths tested");
if (!targetTree) {
return import(defaultTree + "/" + componentName);
}
return import("./components/" + targetTree + "/" + componentName).catch(
() => {
const newTreeArr = targetTree.split("/");
newTreeArr.pop();
const newTree = newTreeArr.join("/");
return recursiveImport(componentName, newTree, defaultTree);
}
);
};
export default function App() {
const targetTree = "custom/client1";
const Component = lazy(() => recursiveImport("Test", targetTree));
return (
<div>
<Suspense fallback="loading">{<Component />}</Suspense>
</div>
);
}
Folder structure:
This solves all of your requirements?
Simple and objective
const Recipe = React.lazy(() =>
import(`docs/app/Recipes/${props.componentName}`)
.catch(() => ({ default: () => <div>Not found</div> }))
);
I was trying something, endup with a simple solution that you should reach before:
https://codesandbox.io/s/awesome-violet-fr7np?file=/src/App.js
I would like to hide two components in the Home component:
DisplayBox and GameBox.
When the user logs in the game starts automatically.
Instead, I'd like to only 'Show' the start button.
Then the user may press the Start button to start the game.
(will eventually have more levels to choose from in the start button component)
import React, { useState, useEffect } from "react";
import "./home.js";
import DisplayBox from '../components/displayBox';
import GameBox from '../components/gameBox/gameBox';
import randomWords from 'random-words'
import "./home.css";
const Home = () => {
const [numLetters, setNumLetters] = useState(5)
const [word, setWord] = useState("")
const [blank, setBlank ] = useState()
console.log("Blank", blank);
console.log("WORD", word)
const getARandomWord = () => {
setWord(randomWords(({ exactly: 1, maxLength: 4, formatter: (word) => word.toUpperCase() })))
}
useEffect(() => {
getARandomWord()
}, [])
function clickStart(){
// return { show: true};
// alert('You Start!');
}
return (
<>
<div>
<button onClick={clickStart} style={{width:"800px"}}>
START
</button>
</div>
<DisplayBox word={word} />
<GameBox numLetters={numLetters} setNumLetters={setNumLetters} word={word} setWord={setWord} getARandomWord={getARandomWord} />
</>
);
};
Home.propTypes = {};
export default Home;
create a new state to oversee whether the game is start or not then:-
Home.js:-
import React, { useState, useEffect } from "react";
import "./home.js";
import DisplayBox from '../components/displayBox';
import GameBox from '../components/gameBox/gameBox';
import randomWords from 'random-words'
import "./home.css";
const Home = () => {
const [numLetters, setNumLetters] = useState(5)
const [word, setWord] = useState("")
const [blank, setBlank ] = useState()
// state to track whether the game is start or not
const [isStart, setIsStart] = useState(false)
console.log("Blank", blank);
console.log("WORD", word)
const getARandomWord = () => {
setWord(randomWords(({ exactly: 1, maxLength: 4, formatter: (word) => word.toUpperCase() })))
}
useEffect(() => {
getARandomWord()
}, [])
// handle game start
function handleGameStart() {
if(isStart) {
// do something when game start
alert('Game starting!!!')
// reset the game again
// setIsStart(false)
} else {
console.log('Game is not starting!')
}
}
// function to oversee what happens after game start
useEffect(() => {
handleGameStart()
}, [isStart])
return (
<>
<div>
<button onClick={() => setIsStart(true)} style={{width:"800px"}}>
START
</button>
</div>
{/* this should only be available when game has started */}
{isStart && (
<>
<DisplayBox word={word} />
<GameBox numLetters={numLetters} setNumLetters={setNumLetters} word={word} setWord={setWord} getARandomWord={getARandomWord} />
</>
)}
</>
);
};
Home.propTypes = {};
export default Home;
I have a context named StatusContext like this:
export const statusCtxInit = {
open: false,
toggleOpen() {
this.open = !this.open;
}
};
const StatusContext = React.createContext(statusCtxInit);
export default StatusContext
The whole app is wrapping with the provider:
// ...
<StatusContext.Provider value={statusCtxInit}>
// ...
To use the values of my context I use useContext in my FC and it works when I get the value.
function MyComp() {
const status = useContext(StatusContext);
return (
<div>
{status.open
? `It's Open`
: `It's Closed`}
<button onClick={() => status.toggleOpen()}>
Toggle
</button>
</div>
)
}
export default MyComp
On the other hand, I also want to change the context by calling the toggleOpen but, it does not work as I want. Actually the value changes but not effect the MyComp.
What I did wrong? What shall I do?
import React from 'react';
const statusContext = React.createContext();
const {Provider} = statusContext;
// custom provider
export const StatusProvider = ({children}) => {
const [isOpen, setOpen] = React.useState(false)
const toggle = () => setOpen(v => !v)
return (
<Provider value={{isOpen, toggle}}>
{children}
</Provider>
)
}
//custom hook
export const useStatus = () => React.useContext(StatusContext)
//usage
function MyComp() {
const status = useStatus()
return (
<div>
{status.isOpen
? `It's Open`
: `It's Closed`}
<button onClick={() => status.toggle()}>
Toggle
</button>
</div>
)
}
export default MyComp