How to use import () to dynamically load components in React? - reactjs

I tried to use import() instead of React.lazy to dynamically load components, but it didn't work.
App.js
import React, { useState } from 'react';
function App() {
const [Com, setCom] = useState(null);
const handleClick = () => {
import("./A.js").then(c => {
//console.log(c.default)
setCom(c.default)
})
}
return (
<div>
<button onClick={handleClick}>Load</button>
{ Com ? <Com /> : null }
</div>
);
}
export default App;
A.js
import React from "react";
export default function A () {
return (<div>A</div>)
}
Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
In fact, I printed out c.default. It's really a function.
c.default
ƒ A() {
return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
__source: {
fileName: _jsxFileName,
lineNumber: 4
},
__self: this
}, "A");
}

Actually, it's not related to the dynamic import but the useState implementation. For example:
import Comp from "./A";
function App() {
const [Com, setCom] = useState(Comp);
return (
<div>
<Com />
</div>
);
}
Will throw the same error. That's because when you call useState(Comp), Comp "function" (which is the Component) been execute (you can test it by removing any <Com /> from the code and adding console.log inside A. The console.log still appears).
As a result Com is no longer a Component but a JSX element. When you try to render JSX element in a function way (meaning wrapping it with < and />, this error thrown.
The solution is either to set Com as a component (() => Com) or to render it as a JSX child ({Com})
import React, { useState } from "react";
function App() {
const [Com, setCom] = useState(null);
const handleClick = () => {
import("./A.js").then(c => {
//console.log(c.default)
setCom(c.default);
});
};
return (
<div>
<button onClick={handleClick}>Load</button>
{/* <Comp /> */}
{Com}
</div>
);
}
export default App;
https://codesandbox.io/s/answer-for-httpsstackoverflowcomq62125854863110-jj2wu
BTW, you can see the difference by console.dir the original outcome of import Comp from "./A"; and the outcome of useState(Comp)
import Comp from "./A";
function App() {
const [Com, setCom] = useState(Comp);
// console.log(1, Com, 2, Comp);
console.dir(Com) // Object
console.dir(Comp) // ƒ A()
//...
}

If setState is called with a function, it will be executed with the previous value as the argument.
const [count,setCount] = useState(0)
setCount(prevCount => prevCount + 1)
In your case, rather than updating the state with the imported function, React will execute it and the result will be set as the state.
Solution:
setCom(() => c.default)

Related

Push state of reactive value to dependencies

I have an object: dynamicJSON that is changing. I would like to pass this object down to multiple dependencies: componentA, componentB. I also want the parts of the dependencies using the object to render when the object is changed.
I tried the useContext Hook, but received a dependency cycle error. What is the proper way to pass reactive values down to dependencies in react?
App.js
import { componetA } from "compA"
import { componetB } from "compB"
import { fetchLatestValue} from "api/fetchLatestValue"
import { useEffect } from "react"
export default function App() {
const dynamicJSON = ???;
useEffect(() => {
let timeoutId;
async function getlatestValue() {
try {
const data = await fetchLatestValue();
// update dynamicJSON here.
} catch (error) {
}
timeoutId = setTimeout(getlatestValue, 1000 * 1);
}
getlatestValue();
return () => {
clearTimeout(timeoutId);
};
}, []);
return (
<componetA />
<componetB />
);
}
compA
export default function componentA() {
const dynamicJSON = ???;
return(
<div>
{dynamicJSON.value}
</div>
)
};
Have you tried useEffect() with a dependency array? If anything in the dependency array is changed, the hook function will be triggered.
Reference: https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect
Sorry I mis-read your question, you should pass dynamicJSON into both components as a prop. Make dynamicJSON a state is also a good idea.
Rule of thumb: if a prop or state of a component is changed, then this component is rerendered.
import { ComponentA } from "compA";
import { ComponentB } from "compB";
import { useEffect, useState } from "react";
export default function App() {
const [dynamicJSON, setDynamicJSON] = useState({});
//...omit
return (
<ComponentA dynamicJSON={dynamicJSON}/>
<ComponentB dynamicJSON={dynamicJSON}/>
);
}
CompA.js
export default function ComponentA(props) {
const { dynamicJSON } = props;
return(
<div>
{dynamicJSON.value}
</div>
)
};

How do I get a state variable from child to parent?

I am learning React.
I have Component structure like this -
index.js
import React from "react";
import Button from "./Button/Button"
export default function Index() {
return (
<>
<Button />
<div>Value of flag in Index.js = {}</div>
</>
);
}
Button.js
import React, { useState } from "react";
import "./button.css";
export default function Button(props) {
const [flag, setFlag] = useState(true);
const clickHandler = () => {
setFlag(!flag);
};
return (
<div className="btn" onClick={clickHandler}>
Value of flag in Button.js = {flag.toString()}
</div>
);
}
My question is "How do I get flag value from Button.js to index.js" ? (child to parent).
1) You can lift state in parent component and pass state and handler as a prop to children.
Note: This is will work because you need flag in the JSX, But if you will pass event handler as a prop in the child component then you have to invoke the handler to get the value. So Either lift the state or use Redux
Live Demo
App.js
const App = () => {
const [flag, setFlag] = useState( true );
return (
<>
<Button flag={flag} setFlag={setFlag} />
<div>Value of flag in Index.js = { flag.toString() }</div>
</>
);
};
Button.js
export default function Button({ flag, setFlag }) {
const clickHandler = () => {
setFlag(oldFlag => !oldFlag);
};
return (
<div className="btn" onClick={clickHandler}>
Value of flag in Button.js = {flag.toString()}
</div>
);
}
2) You can pass handler as a prop in child component as shown in the Harsh Patel answer
3) You can use state management tool i.e. Redux.
You can send a value by method, refer to this:
index.js
import React from "react";
import Button from "./Button/Button"
export default function Index() {
let getFlagValue = (flag) => {
//here you'll get a flag value
console.log(flag)
}
return (
<>
<Button sendFlagValue=(getFlagValue)/>
<div>Value of flag in Index.js = {}</div>
</>
);
}
Button.js
import React, { useState } from "react";
import "./button.css";
export default function Button(sendFlagValue) {
const [flag, setFlag] = useState(true);
const clickHandler = () => {
setFlag(!flag);
sendFlagValue(flag)
};
return (
<div className="btn" onClick={clickHandler}>
Value of flag in Button.js = {flag.toString()}
</div>
);
}
There are two types state:
global state for all
private state for component
For starter, you must obey some policies, not try abuse state, otherwise you will have some troubles.
For global STATE, you can use Redux or dva or umijs.
For private STATE, you seems already known.

React native typescript: usecontext functions not firing from inside child component

I have an issue when i try to use functions from a context inside a child component in a React native android app.
Below is my code for the context, and the form component im using it in (stripped down for brevity).
The "isFormOpen" object can be read no problem from inside any children that is wrapped in the provider, but when i try to call the "toggleForm" function from the same child component, it does nothing, no console errors either.
I have another context which is identical in structure and syntax except for vairable and function names etc, and that works perfectly, so im a bit confused as to why this does not work. I removed the other context, thinking there might be some type of conflict, but didnt solve it.
AccountContext.tsx
import React, { FC, createContext, useContext, useState } from 'react';
interface AccountContextType {
isFormOpen: boolean,
toggleForm: (toggle: boolean) => void
};
export const AccountContext = createContext<AccountContextType>({
isFormOpen: false,
toggleForm: () => null
});
export const AccountContextProvider: FC = props => {
const [formOpen, setFormOpen] = useState<boolean>(false);
const toggleForm = (toggle: boolean) => {
setFormOpen(toggle);
}
const value: AccountContextType = {
isFormOpen: formOpen,
toggleForm
}
return (
<AccountContext.Provider value={value}>
{props.children}
</AccountContext.Provider>
)
}
export const useAccountContext = () => useContext(AccountContext);
TrackUploadForm.js
import React from 'react';
import { SafeAreaView } from 'react-native';
import { Button } from 'react-native-paper';
import { useAccountContext } from '../contexts/AccountContext';
import { AccountContextProvider } from '../contexts/AccountContext';
const TrackUploadForm = () => {
const accountContext = useAccountContext();
return (
<AccountContextProvider>
<SafeAreaView>
<Button onPress={() => accountContext.toggleForm(false)} mode='outlined'>Cancel</Button>
</SafeAreaView>
</AccountContextProvider>
)
};
export default TrackUploadForm;
useAccountContext is called outside the provider
export default function App() {
return (
<AccountContextProvider>
<Content />
</AccountContextProvider>
);
}
const Content = () => {
const accountContext = useAccountContext();
return (
<div className="App">
<h1>{accountContext.isFormOpen ? "true" : "false"}</h1>
<Button onPress={() => accountContext.toggleForm(false)} mode='outlined'>Cancel</Button>
</div>
);
};
accountContext.toggleForm(false) <-- always false, change it to accountContext.toggleForm(!accountContext.isFormOpen)
Together we have
https://codesandbox.io/s/cranky-panini-yo129

How to test code that uses a custom hook based on useContext with react-testing-library and jest

I've created a custom context hook - and I'm struggling to figure out how to pass values to its provider during testing.
My hook:
import React, { createContext, useContext, useState } from 'react';
const Context = createContext({});
export const ConfigurationProvider = ({ children }) => {
// Use State to keep the values
const [configuration, setConfiguration] = useState({});
// pass the value in provider and return
return (
<Context.Provider
value={{
configuration,
setConfiguration,
}}
>
{children}
</Context.Provider>
);
};
export const useConfigurationContext = () => useContext(Context);
export const { Consumer: ConfigurationConsumer } = Context;
This is how it's used in the application:
function App() {
return (
<ConfigurationProvider>
<div className="app">
<ComponentA />
</div>
</ConfigurationProvider>
);
}
And in ComponentA:
const ComponentA = () => {
// Get configuration
const configuration = useConfigurationContext();
return (
<div>{JSON.stringify(configuration)}</div>
)
}
This all works fine - considered that I'm calling setConfiguration from another component and set an object. Now for the testing part:
import React, { Component, createContext } from 'react';
import { render, waitFor } from '#testing-library/react';
import ComponentA from 'componentA';
const config = {
propertyA: 'hello',
};
test('renders the config', async () => {
const ConfigurationContext = createContext();
const { queryByText } = render(
<ConfigurationContext.Provider value={config}>
<ComponentA />
</ConfigurationContext.Provider>
);
expect(queryByText('hello')).toBeInTheDocument();
});
This doesn't work - I'm expecting the value that I'm sending in would be rendered in the div, but the context is an empty object. What am I doing wrong?
Thanks to Carle B. Navy I got the reason why it doesn't work. For other people two wonder what the solution is I fixed it by doing the following:
In my context hook, I changed the last line to export the provider as well:
export const { Consumer: ConfigConsumer, Provider: ConfigProvider } = Context;
Then in my test case, instead of creating a new context, I import the ConfigProvider at the top, and then:
const { queryByText } = render(
<ConfigProvider value={config}>
<ComponentA />
</ConfigProvider>
);
Thanks for helping me solve this and hope this helps someone else.

On click returns null instead of an object

It's really basic I guess. I'm trying to add onClick callback to my script & I believe I'm missing a value that would be responsible for finding the actual item.
Main script
import React from 'react';
import { CSVLink } from 'react-csv';
import { data } from 'constants/data';
import GetAppIcon from '#material-ui/icons/GetApp';
import PropTypes from 'prop-types';
const handleClick = (callback) => {
callback(callback);
};
const DownloadData = (props) => {
const { callback } = props;
return (
<>
<CSVLink
data={data}
onClick={() => handleClick(callback)}
>
<GetAppIcon />
</CSVLink>
</>
);
};
DownloadData.propTypes = {
callback: PropTypes.func.isRequired,
};
export default DownloadData;
Storybook code
import React from 'react';
import DownloadData from 'common/components/DownloadData';
import { data } from 'constants/data';
import { action } from '#storybook/addon-actions';
export default {
title: 'DownloadData',
component: DownloadData,
};
export const download = () => (
<DownloadData
data={data}
callback={action('icon-clicked')}
/>
);
So right now with this code on click in the storybook I'd get null and I'm looking for an object.
One of the potential issues I can see is that your handleClick function is stored as it is in-memory, when you import the component. That means you're keeping reference of something that doesn't exists and expects to use it when rendering the component with the callback prop.
Each instance of a component should have its own function. To fix it, move the function declaration inside the component. Like this:
const Foo = ({ callback }) => {
// handleClick needs to be inside here
const handleClick = callback => {
console.log("clicked");
callback(callback);
};
return <div onClick={() => handleClick(callback)}>Click me!</div>;
};
Check this example.
If this doesn't fix your problem, then there is something wrong with how you're implementing Storybook. Like a missing context.

Resources