React lazy recursive import - reactjs

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

Related

Is possible to build compound component with separated files? REACT

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
}

React Context is not updating

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>
);
}

dynamically load images with react

I created a component that renders images
this is the component.
import React, { lazy, Suspense } from "react";
const Icon = (props) => {
const { src } = props;
return (
<img src={src} />
);
};
export default Icon;
then I use it like this
import ExampleIcon from "./../images/icons/example.png";
...
<Icon src={ExampleIcon} />
is there a more efficient way to load the icons?
and then just "load" example.png and use it as a source? tried to change it to:
const Icon = (props) => {
const src = lazy(() => import("./../images/icons/" + props.src + ".png"));
return (
<Suspense fallback={<p>loading...</p>}><img src={src} /></Suspense>
);
};
looks like it doesnยดt work that way. any other ideas? thanks!
No, you can't do this, since React.lazy() must be at the top level and only return React components. To lazily load images you can do inside an effect:
function Icon = props => {
const [image, setImage] = useState()
useEffect(() => {
import("./../images/icons/" + props.src + ".png").then(setImage)
}, [props.src])
return image ? <img src={image} /> : 'Loading...'
}
Edit: there's one little problem with this, namely, Webpack will not be able to figure out the file to code split since the string in the import function is not a literal. You could still access files in the public directory dynamically using fetch instead. And perhaps you don't need fetch at all, just provide an url to src and spare the whole hassle.
You could apply this approach:
Preloading images with JavaScript
const img=new Image();
img.src=url;
And how to do it with a hook with an online example:
https://www.selbekk.io/blog/2019/05/how-to-write-a-progressive-image-loading-hook/
Another approach just using hooks:
https://codesandbox.io/s/magical-pine-419kz?file=/src/App.tsx
import React, { useEffect } from "react";
import "./styles.css";
const loadImg = (src: string): Promise<string> =>
new Promise((resolve, reject) => {
const img = new Image();
img.src = src;
img.onload = () => resolve(src);
img.onerror = () => reject(new Error("could not load image"));
});
export default function App() {
const [src, setSrc] = React.useState("preloadimg");
useEffect(() => {
const load = async () => {
await loadImg(
"https://upload.wikimedia.org/wikipedia/commons/thumb/8/8c/Chess_Large.JPG/800px-Chess_Large.JPG"
).then((src) => {
setSrc(src);
});
}; // Execute the created function directly
load();
}, [src, setSrc]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<img src={src} alt="example" />
</div>
);
}

In React, is there an elegant way of using the id in a RESTful edit url and loading the corresponding object into the initial state of my component?

I'm building a React 16.13 application. I have a search component, src/components/Search.jsx, that constructs search results and then builds a URL to edit those results ...
renderSearchResults = () => {
const { searchResults } = this.state;
if (searchResults && searchResults.length) {
return (
<div>
<div>Results</div>
<ListGroup variant="flush">
{searchResults.map((item) => (
<ListGroupItem key={item.id} value={item.name}>
{item.name}
<span className="float-right">
<Link to={"/edit/"+item.id}>
<PencilSquare color="royalblue" size={26} />
</Link>
</span>
</ListGroupItem>
))}
</ListGroup>
</div>
);
}
};
render() {
return (
<div className="searchForm">
<input
type="text"
placeholder="Search"
value={this.state.searchTerm}
onChange={this.handleChange}
/>
{this.renderSearchResults()}
</div>
);
}
Is there a more elegant way to load/pass the object I want to edit? Below I'm deconstructing the URL and launching an AJAX call but what I'm doing seems kind of sloppy. I'm familiar with Angular resolvers and that seems a cleaner way of decoupling the logic of parsing the URL and finding the appropriate objects but the below is all I could come up with ...
src/components/Edit.jsx
import React, { Component } from "react";
import FormContainer from "../containers/FormContainer";
export default class Edit extends Component {
render() {
return <FormContainer />;
}
}
src/containers/FormContainer.jsx
class FormContainer extends Component {
...
componentDidMount() {
let initialCountries = [];
let initialProvinces = [];
let coopTypes = [];
// Load form object, if present in URL
const url = window.location.href;
const id = url.split("/").pop();
fetch(FormContainer.REACT_APP_PROXY + "/coops/" + id)
.then((response) => {
return response.json();
})
.then((data) => {
const coop = data;
coop.addresses.map(address => {
address.country = FormContainer.DEFAULT_COUNTRY_CODE; // address.locality.state.country.id;
});
this.setState({
newCoop: coop,
});
});
You aren't posting all the relevant code but I know what you are trying to accomplish (correct me if I'm wrong). You want to use the id from the url parameters to fetch data. I think you are using react-router. You can use this example to refactor your code:
import React, { useState, useEffect } from "react";
import {
BrowserRouter as Router,
Switch,
Route,
useParams
} from "react-router-dom";
const REACT_APP_PROXY = "api";
const DEFAULT_COUNTRY_CODE = "20";
// You can use functional components and react hooks in React 16.13 to do everything
// No need for class components any more
function FormContainer() {
// useState hook to handle state in functional components
const [newCoop, setNewCoop] = useState({});
// useParams returns an object of key/value pairs of URL parameters. Use it to access match.params of the current <Route>.
const { id } = useParams();
// This will be called whenever one of the values in the dependencies array (second argument) changes
// but you can pass an empty array to make it run once
useEffect(() => {
fetch(REACT_APP_PROXY + "/coops/" + id)
.then(response => {
return response.json();
})
.then(data => {
const coop = data;
coop.addresses.map(address => {
address.country = DEFAULT_COUNTRY_CODE; // address.locality.state.country.id;
});
setNewCoop(coop);
});
// use an empty array as the second argument to run this effect on the first render only
// it will give a similar effect to componentDidMount
}, []);
return <div>Editing {id}</div>;
}
const Edit = () => <FormContainer />;
function App() {
return (
<Router>
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/edit/:id">
<Edit />
</Route>
</Switch>
</Router>
);
}

React lazy does not cause splitting bundle in chunks

As in the title, I'm trying to use React.lazy feature which works in my my other project. But not in this one, I don't know what I'm missing here. All works just fine, no errors, no warnings. But for some reason I don't see my bundle split in chunks.
Here's my implementation:
import React, { Component, Suspense } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { getApps } from '../../actions/apps';
import './_apps.scss';
const AppItemComponent = React.lazy(() => import('../AppItem'));
class Apps extends Component {
componentDidMount() {
const { getApps } = this.props;
getApps(3);
}
renderAppItem = () => {
const { apps } = this.props;
return apps && apps.map((item, i) => {
return (
<Suspense fallback={<div>loading...</div>} key={i}>
<AppItemComponent
index={i + 1}
item={item}
/>
</Suspense>
);
});
};
render() {
const { apps } = this.props;
return (
<div className="apps__section">
<div className="apps__container">
<div className="apps__header-bar">
<h3 className="apps__header-bar--title">Apps</h3>
<Link className="apps__header-bar--see-all link" to="/apps">{`see all (${apps.length})`}</Link>
</div>
{this.renderAppItem()}
</div>
</div>
);
}
}
const mapStateToProps = ({ apps }) => {
return { apps };
};
const mapDispatchToProps = dispatch => {
return {
getApps: quantity => dispatch(getApps(quantity)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Apps);
I'm doing this in react-create-app app and in react v16.6, react-dom v16.6.
What am I missing here?
I also have the same problem, then I have resolved this case without using Suspense and lazy(try code below), and I can see chunks file. However, after using this way, I try changing my code again with Suspense and lazy. It works!!! and I don't know why it does. Hope that it works for someone find solution for this case.
1 - create file asyncComponent
import React, { Component } from "react";
const asyncComponent = (importComponent) => {
return class extends Component {
state = {
component: null,
};
componentDidMount() {
importComponent().then((cmp) => {
this.setState({ component: cmp.default });
});
}
render() {
const C = this.state.component;
return C ? <C {...this.props} /> : null;
}
};
};
export default asyncComponent;
2 - and in App.js, example:
const AuthLazy = asyncComponent(() => import("./containers/Auth/Auth"));
//Route
<Route path="/auth" component={AuthLazy} />
Check that your component is not imported somewhere with regular import: import SomeComponent from './path_to_component/SomeComponent';
Check that component is not re-exported somewhere. (For example in index.js (index.ts) file: export * from './SomeComponent') If so, just remove re-export of this component.
Check that your export your component as default or use code like this:
const SomeComponent = React.lazy(() => import('./path_to_component/SomeComponent').then((module) => ({ default: module.SomeComponent })));

Resources