Why are there two different ways to use it? - reactjs

the same component is stored in state and ref respectively, but the calling methods of the two are different.
export default function getAsyncComponent(load) {
const AsyncComponent = () => {
const [component, setComponent] = useState(null)
const com = useRef(null)
const loadComponent = async () => {
const _component = await load()
com.current = _component.default
setComponent(_component.default)
}
useEffect(() => {
loadComponent()
}, [])
console.log(component, com.current)
const Com = com.current
return (
<div>
// Both ways are works.
{component ? component : null}
{Com ? <Com /> : null}
</div>
)
}
return <AsyncComponent />
}
print:
{$$typeof: Symbol(react.element), type: 'div', key: null, ref: null, props: {…}, …}
and
ƒ IndexPage() {
return ((0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)("div", { children: [(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("img", { className: _index_less__WEBPACK_I…
The use of getAsyncComponent is like this.
function App() {
return (
<BrowserRouter>
<Routes>
<Route
path="/"
element={getAsyncComponent(() => import('src/pages/index'))}
/>
</Routes>
</BrowserRouter>
)
}
Are the above two calling methods different?
react#18.1
react-router-dom#6.3

Related

Setting value for React context values with React testing library

I am working on testing a component using react-testing-library. The component has an alert which accepts a prop that comes from context to determine whether the alert is open or not.
const PersonRecord = () => {
const {
personSuccessAlert,
setPersonSuccessAlert,
updatePersonSuccessAlert,
setUpdatePersonSuccessAlert,
} = useContext(PeopleContext);
return (
{personSuccessAlert && (
<div className="person-alert-container">
<Alert
ariaLabel="person-create-success-alert"
icon="success"
open={personSuccessAlert}
/>
</div>
)}
)
}
So the above code uses context to pull the value of personSuccessAlert from PeopleContext. If personSuccessAlert is true the alert will display. My context file is set up as follows:
import React, { createContext, useState } from 'react';
export const PeopleContext = createContext();
const PeopleContextProvider = ({ children }) => {
const [personSuccessAlert, setPersonSuccessAlert] = useState(false);
const [updatePersonSuccessAlert, setUpdatePersonSuccessAlert] = useState(
false,
);
return (
<PeopleContext.Provider
value={{
personSuccessAlert,
updatePersonSuccessAlert,
setPersonSuccessAlert,
setUpdatePersonSuccessAlert,
}}>
{children}
</PeopleContext.Provider>
);
};
export default PeopleContextProvider;
Now I am trying to develop a test which passes personSuccessAlert = true to the PersonRecord component.
Here is what I have been trying:
export function renderWithEmptyPerson(
ui,
{
providerProps,
path = '/',
route = '/',
history = createMemoryHistory({ initialEntries: [route] }),
},
) {
return {
...render(
<MockedProvider mocks={getEmptyPerson} addTypename={false}>
<PeopleContextProvider {...providerProps}>
<Router history={history}>
<Route path={path} component={ui} />
</Router>
</PeopleContextProvider>
</MockedProvider>,
),
};
}
describe('empty person record rendering', () => {
afterEach(cleanup);
test('empty person', async () => {
const providerProps = { value: true };
const { getByText, queryByText, queryByLabelText } = renderWithEmptyPerson(
PersonRecord,
{
providerProps,
route: 'people/6d6ed1f4-8294-44de-9855-2999bdf9e3a7',
path: 'people/:slug',
},
);
expect(getByText('Loading...')).toBeInTheDocument();
});
});
I have tried different variations of const providerProps = { value: true };. Replacing value with personSuccessAlert did not work.
Any advice or help is appreciated.
You are passing providerProps to the PeopleContextProvider, but the PeopleContextProvider is not doing anything with the props. You'll need to actually use those props, for example to set the initial state. You could try something like:
const PeopleContextProvider = ({ children, initialPersonSuccessAlert = false }) => {
const [personSuccessAlert, setPersonSuccessAlert] = useState(initialPersonSuccessAlert);
const [updatePersonSuccessAlert, setUpdatePersonSuccessAlert] = useState(
false,
);
return (
<PeopleContext.Provider
value={{
personSuccessAlert,
updatePersonSuccessAlert,
setPersonSuccessAlert,
setUpdatePersonSuccessAlert,
}}>
{children}
</PeopleContext.Provider>
);
};
This would allow you to set the initial state of personSuccessAlert by passing in a initialPersonSuccessAlert prop. You could update your test like so:
const providerProps = { initialPersonSuccessAlert: true };
Alternatively, if you only wanted to make changes in your test file, you could consider updating the renderWithEmptyPerson function to use PeopleContext.Provider directly instead of the PeopleContextProvider component. That will allow you to set the context value however you like.

Passing props to child component with react-router v6

I am using React Router v6 with all routes in a single config file, hence, making use of <Outlet /> to render the child. However I don't know how to pass the appropriate props depending on the component that matches when having my routes in a separate config file.
// route.ts
export const routes = [
{
path: '/*',
element: <Parent />,
children: [
{
path: 'child1',
elements: <Child1 />, // Typescript complains: Property 'firstName' is missing in type '{}' but required in type 'IProps'
},
{
path: 'child2',
elements: <Child2 />, // Property 'lastName' is missing in type '{}' but required in type 'IProps'
}
]
}
]
// App.ts
const App = () => {
const _routes = useRoutes(routes)
return _routes
}
// Parent.ts
const Parent = () => {
const firstName = 'John'
const lastName = 'Doe'
return (
<h1>Parent Component</h1>
<Outlet /> // How to pass the appropriate props?
)
}
interface IFirstNameProps {
firstName: string
}
interface ILastNameProps {
lastName: string
}
export const Child1 = (props: IProps) => {
return (
<h2>First Name</h2>
{props.firstName}
)
}
export const Child2 = (props: IProps) => {
return (
<h2>Last Name</h2>
{props.lastName}
)
}
Using the <Route> component it would be something like this:
const Parent = () => {
const firstName = 'John'
const lastName = 'Doe'
return (
<Route path='child1' element={
<Child1 firstName={firstName} />
} />
<Route path='child2' element={
<Child2 lastName={lastName} />
} />
)
}
How can I achieve the same thing with a routes.ts config file and <Outlet />?
The best way to deal with this situation is by using outlet context.
In the parent component pass context via Outlet.
// parent.ts
const Parent = () => {
const firstName = 'John'
const lastName = 'Doe'
return (
<h1>Parent Component</h1>
<Outlet context=[firstName, lastName]/> // How to pass the appropriate props?
)
}
Then, in the children components, get those context data using useOutletContext hook.
import { useOutletContext } from "react-router-dom";
export const Child1 = () => {
const [firstName] = useOutletContext();
return (
<h2>First Name</h2>
{props.firstName}
)
}
export const Child2 = () => {
const [lastName] = useOutletContext();
return (
<h2>Last Name</h2>
{props.lastName}
)
}

What type to use when implement RouteComponentProps on react typescript component

I am trying to make my first component that receive parameter using react-router-dom.
This is my component:
const ProductDetailsPage: React.FC<IPage & RouteComponentProps<any>> = (props: Props) => {
useEffect(() => {
const id = props.match.params.id;
}
return <h1>product details</h1>
}
interface Props {
match: {
params: {
id: string | undefined;
};
};
}
Here eslint show me a warning that I should not use any but I don't understand what type to set here.
Also I wrote props: Props but I don't know if this is correct way to do it like this.
UPDATE more code for preview:
export default interface IPage {
name: string;
}
const routes: IRoute[] = [
{
path: '/products/:id',
name: 'details',
component: ProductDetailsPage,
exact: true,
},
];
const ProductsPage: React.FC<IPage> = () => {
const match = useRouteMatch();
return (
<div>
<h4>This is products page</h4>
<div>
<Link to={`${match.url}/44`}>Product 44</Link>
<Link to={`${match.url}/45`}>Product 44</Link>
<Link to={`${match.url}/46`}>Product 44</Link>
</div>
<Switch>
{routes.map((route, index) => {
return (
<Route
key={index}
path={route.path}
render={(props: RouteComponentProps<any>) => (
<route.component name={route.name} {...props} {...route.props} />
)}
/>
);
})}
</Switch>
</div>
);
};
interface Props {
match: {
params: {
id: string | undefined;
};
};
}
const ProductDetailsPage: React.FC<IPage & RouteComponentProps<Props>> = (props) => {
const [message, setMessage] = useState<string>('');
useEffect(() => {
const id = props.match.params.id;
if (id) {
setMessage(`Show details for product: ${id}`);
} else {
setMessage(`No parameter passed`);
}
}, [props.match.params.id]);
return <h4>{message}</h4>;
};
export default withRouter(ProductDetailsPage);
The generic (T) in RouteComponentProps<T> defines the params type.
A param value is always a string or undefined.
If the params are unknown it would probably be better to use Record<string, string | undefined> to type the params.
type Params = { id: string | undefined; };
const Test: React.FC<RouteComponentProps<Params>> = ({
match: { params: { id } }
}) => {
const idIsStringOrUndefined: string | undefined = id;
return null;
}
Also props should be typed automatically by typing the component with React.FC<RouteComponentProps<Params>>.
You can refer to this Playground to understand RouteComponentProps.
type TParams = {id:string | undefined};
type TProductDetailsProps = IPage & RouteComponentProps<TParams>
const ProductDetailsPage: React.FC<TProductDetailsProps> = (props) => {
useEffect(() => {
const id = props.match.params.id;
});
return <h1>product details</h1>
}

Problem when dynamically registering routes in an application with microfrontends concept

I have an Typescript + Redux (with RTK) application using the microfrontends concept. All the steps for the construction came from this tutorial: Microfrontends tutorial.
The main component is Microfrontend.tsx (omitted imports):
interface Manifest {
files: {
'main.js': string
'main.js.map': string
'index.html': string
}
entrypoints: string[]
}
const MicroFrontend = ({
name,
host,
module
}: {
name: string
host: string | undefined
module: string
}) => {
const history = useHistory()
useEffect(() => {
const renderMicroFrontend = () => {
// #ts-ignore
window[`render${name}`] && window[`render${name}`](`${name}-container`, history)
}
if (document.getElementById(name)) {
renderMicroFrontend()
return
}
const manifestUrl = `${
isDevProfile ? host : ''
}/${module}/view/asset-manifest.json`
fetch(manifestUrl)
.then(res => res.json())
.then((manifest: Manifest) => {
const script = document.createElement('script')
script.id = name
script.crossOrigin = ''
script.src = `${host}${manifest.files['main.js']}`
script.onload = () => {
renderMicroFrontend()
}
document.head.appendChild(script)
})
return () => {
// #ts-ignore
window[`unmount${name}`] && window[`unmount${name}`](`${name}-container`)
}
})
return (
<main id={`${name}-container`} style={{ height: '100%' }} />
)
}
MicroFrontend.defaultProps = {
document,
window
}
export default MicroFrontend
I'm trying to render the routes of the child components in a dynamic way, however, when I do this, I have a very strange effect: Bug.
The code snippet that generates this effect is this (omitted imports):
const App = () => {
const dispatch = useAppDispatch()
const { loadWithSuccess } = useSelector(moduleSelectors)
const avaibleModuleLinks = useSelector(avaibleModuleLinksWhitoutHome)
useEffect(() => {
dispatch(fetchAvaibleModules()).then(response =>
dispatch(fetchAvaibleModuleLinks(response.payload as string[]))
)
}, [dispatch])
return (
<BrowserRouter>
<Template>
<Switch>
<Route exact={true} path="/" component={Home} />
{loadWithSuccess ? avaibleModuleLinks?.map(
(subMenuPath: SubMenuPath | undefined, index: number) => {
const subMenuPathKey = subMenuPath ? subMenuPath.key : ''
let micro = () => (
<MicroFrontend
module={subMenuPathKey}
host="127.0.0.1"
name={subMenuPath ? subMenuPath.key.charAt(0).toUpperCase() : ''}
/>
)
return (
<Route
key={index}
path={`/dfe/view/${subMenuPathKey}`}
component={micro}
/>
)
}
): <></>}
</Switch>
</Template>
</BrowserRouter>
)
}
export default App
Only when I don't render routes dynamically do I have the desired effect: desired behavior
The code snippet that generates this effect is this (omitted imports):
const ModuleNfe = () => (
<MicroFrontend host="127.0.0.1" name="Nfe" module="nfe" />
)
const App = () => {
const dispatch = useAppDispatch()
const { loadWithSuccess } = useSelector(moduleSelectors)
const avaibleModuleLinks = useSelector(avaibleModuleLinksWhitoutHome)
useEffect(() => {
dispatch(fetchAvaibleModules()).then(response =>
dispatch(fetchAvaibleModuleLinks(response.payload as string[]))
)
}, [dispatch])
return (
<BrowserRouter>
<Template>
<Switch>
<Route exact={true} path="/" component={Home} />
<Route path="/dfe/view/nfe" component={ModuleNfe} />
</Switch>
</Template>
</BrowserRouter>
)
}
export default App
As you may have noticed, the desired behavior is for my page to be rendered inside the Template component. But for some reason, this is not the case.

How to pass data queried in one component to another component to use as a query variable?

I'm stuck trying to pass a value queried with Apollo Client in one route component to another route component to use as a variable in a query. The exact error is: "Uncaught TypeError: Cannot read property 'name' of undefined".
There are three components:
App, the root component with the router.
ComponentA, that queries a group of data by id and name to show a list of Cards for each item. Every item has a link to a to ComponentB.
Component B, which has to query more data using the name referenced by ComponentA as a variable to show more data from that item.
App.tsx
export const App: React.FunctionComponent = () => {
return (
<BrowserRouter>
<>
<Main>
<Switch>
<Route exact path="/" component={ComponentA} />
<Route path="/:name" component={ComponentB} />
</Switch>
</Main>
</>
</BrowserRouter>
);
};
ComponentA.tsx
const GET_DATAS = gql`
query GetDatas {
getDatas {
_id
name
}
}
`;
interface Data {
_id: string;
name: string;
}
export const Home: React.FunctionComponent = () => {
const { data } = useQuery(GET_DATAS);
return (
<>
<div>
{data.getDatas.map((data: Data) => (
<Link to={`/${data.name}`} key={data._id}>
<Card name={data.name} />
</Link>
))}
</div>
</>
);
};
ComponentB.tsx
const GET_DATA = gql`
query GetData($name: String!) {
getData(name: $name) {
_id
name
year
color
}
}
`;
interface Props {
name: string;
}
export const DataDetails: React.FunctionComponent<Props> = (props: Props) => {
const { data } = useQuery(GET_DATA, {
variables: { name },
});
return (
<>
<div>
<H1>{data.getData.name}</H1>
<p>{data.getData.year}</p>
<p>{data.getData.color}</p>
</div>
</>
);
};
The queries work well as I tested them in Playground, and I tried using local state and passing the props with Link with no results, but I still can't figure out how to pass the value to use in the query of ComponentB.
Thanks in advance!
Fixed 🎉 I finally opted for just taking the URL, cleaning it a bit, and using it as a variable for the query, and also adding the loading and error states:
export const DataDetails: React.FunctionComponent = () => {
const dirtyPath = location.pathname;
const cleanPath = dirtyPath.replace(/%20/g, ' ').replace(/\//g, '');
const { data, loading, error } = useQuery(GET_DATA, {
variables: { name: cleanPath },
});
return (
...
);
};
Another solution, available when using React Router, would be:
export const DataDetails: React.FunctionComponent = (props) => {
const { data, loading, error } = useQuery(GET_DATA, {
variables: { name: props.match.params.name },
});
return (
...
);
};

Resources