How would one pass a component to a helper? - reactjs

I want to pass a component to a helper and have that helper return an array of objects, each with a component node...
// helpers.ts
import { LINKS } from '../constants';
// error on the next line: Cannot find name 'Component'. ts(2304)
const createLinks = (component: Component) => {
return LINKS.map((props) => {
return ({
content: <Component {...props} />,
id: props.id
});
});
};
// component.tsx
import { List, SpecialLink } from '../components';
import { createLinks } from '../helpers';
const LinkList = () => {
const links = createLinks(SpecialLink);
return <List items={links}>
}

You should use the ComponentType type of react, so the component argument can be class component or function component.
type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>;
import React from 'react';
import { ComponentType } from 'react';
const LINKS: any[] = [];
const createLinks = (Component: ComponentType) => {
return LINKS.map((props) => {
return {
content: <Component {...props} />,
id: props.id,
};
});
};

Related

React Router 6 and TypeScript - rewrite WithRouter to be a typed wrapper [duplicate]

How can i access url parameter in my react component ?
App.js
<Route path="/question/:id" element={<QuestionView />} />
QuestionView.js
class QuestionView extends React.Component {
render() {
const { questions, users } = this.props;
const {id} = ???
Issue
In react-router-dom v6 the Route components no longer have route props (history, location, and match), and the current solution is to use the React hooks "versions" of these to use within the components being rendered. React hooks can't be used in class components though.
To access the match params with a class component you must either convert to a function component, or roll your own custom withRouter Higher Order Component to inject the "route props" like the withRouter HOC from react-router-dom v5.x did.
Solution
I won't cover converting a class component to function component. Here's an example custom withRouter HOC:
const withRouter = WrappedComponent => props => {
const params = useParams();
// etc... other react-router-dom v6 hooks
return (
<WrappedComponent
{...props}
params={params}
// etc...
/>
);
};
And decorate the component with the new HOC.
export default withRouter(Post);
This will inject a params prop for the class component.
this.props.params.id
HOC withRouter TypeScript version with generic Params
withRouter.tsx
import { ComponentType } from 'react';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
export interface WithRouterProps<T = ReturnType<typeof useParams>> {
history: {
back: () => void;
goBack: () => void;
location: ReturnType<typeof useLocation>;
push: (url: string, state?: any) => void;
}
location: ReturnType<typeof useLocation>;
match: {
params: T;
};
navigate: ReturnType<typeof useNavigate>;
}
export const withRouter = <P extends object>(Component: ComponentType<P>) => {
return (props: Omit<P, keyof WithRouterProps>) => {
const location = useLocation();
const match = { params: useParams() };
const navigate = useNavigate();
const history = {
back: () => navigate(-1),
goBack: () => navigate(-1),
location,
push: (url: string, state?: any) => navigate(url, { state }),
replace: (url: string, state?: any) => navigate(url, {
replace: true,
state
})
};
return (
<Component
history={history}
location={location}
match={match}
navigate={navigate}
{...props as P}
/>
);
};
};
MyClass.tsx
import { Component } from 'react';
import { withRouter, WithRouterProps } from './withRouter';
interface Params {
id: string;
}
type Props = WithRouterProps<Params>;
class MyClass extends Component<Props> {
render() {
const { match } = this.props;
console.log(match.params.id); // with autocomplete
return <div>MyClass</div>;
}
}
export default withRouter(MyClass);
Here's the code example I'm using in my project to get the id from the URL:
import React from 'react'
import {Button} from 'antd'
import {useParams} from 'react-router-dom'
const DeleteUser = () => {
const {id} = useParams()
const handleDelete = async () => {
// handle delete function
}
return (
<Button onClick={handleDelete}>Delete User</Button>
)
}
export default DeleteUser
If you would like to use a class, then you will need to wrap it with the withRouter. I provide an example below:
This is my class for the movie form:
class MovieForm extends Form {
state = {
data: {
title: "",
genreId: "",
numberInStock: "",
dailyRentalRate: ""
},
genres: [],
errors: {}
};
schema = {
_id: Joi.string(),
title: Joi.string()
.required()
.label("Title"),
genreId: Joi.string()
.required()
.label("Genre"),
numberInStock: Joi.number()
.required()
.min(0)
.max(100)
.label("Number in Stock"),
dailyRentalRate: Joi.number()
.required()
.min(0)
.max(10)
.label("Daily Rental Rate")
};
componentDidMount() {
const genres = getGenres();
this.setState({ genres });
// const movieId = this.props.match.params.id;
const movieId = this.props.params.id;
if (movieId === "new") return;
const movie = getMovie(movieId);
if (!movie) return this.props.history.replace("/not-found");
this.setState({ data: this.mapToViewModel(movie) });
}
mapToViewModel(movie) {
return {
_id: movie._id,
title: movie.title,
genreId: movie.genre._id,
numberInStock: movie.numberInStock,
dailyRentalRate: movie.dailyRentalRate
};
}
doSubmit = () => {
saveMovie(this.state.data);
this.props.navigate("/movies");
};
render() {
return (
<div>
<h1>Movie Form</h1>
<form onSubmit={this.handleSubmit}>
{this.renderInput("title", "Title")}
{this.renderSelect("genreId", "Genre", this.state.genres)}
{this.renderInput("numberInStock", "Number in Stock", "number")}
{this.renderInput("dailyRentalRate", "Rate")}
{this.renderButton("Save")}
</form>
</div>
);
}
}
I write a wrapper outside of the class:
const withRouter = WrappedComponent => props => {
const params = useParams();
const navigate = useNavigate();
return (
<WrappedComponent
{...props}
params={params}
navigate={navigate}
/>
);
};
Now, at the end of the file I will export it like below:
export default withRouter(MovieForm);
Insdie the withRouter, I get all the functions that I will use later inside the class:
const params = useParams();
const navigate = useNavigate();
TypeScript version
withRouter.tsx
import React from 'react';
import {
useLocation,
useNavigate,
useParams,
NavigateFunction,
Params,
Location,
} from 'react-router-dom';
export interface RouterProps {
router: {
navigate: NavigateFunction;
readonly params: Params<string>;
location: Location;
}
}
function withRouter(Component: React.ComponentType<RouterProps>) {
const ComponentWithRouterProp: React.FC = () => {
const location = useLocation();
const navigate = useNavigate();
const params = useParams();
return (
<Component
router={{ location, navigate, params }}
/>
);
};
return ComponentWithRouterProp;
}
export default withRouter;
MyComponent.tsx
import React from 'react';
import { connect } from 'react-redux';
import { RootState } from '##/redux/store';
import {
addSettings,
updateSettings,
} from '##/redux/mySlice';
import withRouter, { RouterProps } from '##/withRouter';
const mapState = (state: RootState) => ({
myStore: state.variation.myStore,
});
const mapDispatch = {
addSettings,
updateSettings,
};
type IProps = ReturnType<typeof mapState> & typeof mapDispatch & RouterProps;
class MyComponent extends React.Component<IProps> {
constructor(props: IProps) {
super(props);
}
onNavigateHome = () => {
this.props.router.navigate('/');
}
render(): React.ReactNode {
return (
<div className="test" onClick={this.onNavigateHome}>test</div>
);
}
}
export default withRouter(connect(mapState, mapDispatch)(MyComponent));
I had a similar issue, described here:
Params from React Router with class components and typescript
I created a new functional component, so I can use useParams():
import React from 'react';
import { useParams } from 'react-router';
type Props = {
children: JSX.Element
};
export const WithParams: React.FC<Props> = (props) => {
const params = useParams();
return React.cloneElement(props.children, {...props.children.props, ...params });
};
and added it to my Route.element
<Route path="/Contacts/VerifyEmailAddress/:id"
element={
<WithParams>
<VerifyEmail />
</WithParams>
}>
</Route>
and added the parameters I need to the props of my child component.
export class VerifyEmailProps {
public id?: string;
}

React Context value doesn't change

I just finished working on a custom Context Provider and I came across a problem. The context isn't getting updated with the data from the function inside of it. It just stays the same (as specified default value).
I'm not really sure what the bug is :/
Here's the code:
useBreakpoints.tsx
// Functions
import { createContext } from 'react';
// Hooks
import { useContext, useEffect, useState } from 'react';
// Types
import type { ReactNode } from 'react';
type Width = number | undefined;
interface Breakpoints {
[key: number | string]: number;
}
interface Values {
[key: number | string]: boolean;
}
interface IProps {
children: ReactNode;
breakpoints: Breakpoints;
}
// Context
export const BreakpointsContext = createContext({});
export const BreakpointsProvider = ({ children, breakpoints }: IProps) => {
const [width, setWidth] = useState<Width>(undefined);
const [values, setValues] = useState<Values | undefined>(undefined);
useEffect(() => {
if (typeof window !== 'undefined') {
const handleResize = () => {
setWidth(window.innerWidth);
};
window.addEventListener('resize', handleResize);
handleResize();
return () =>
window.removeEventListener('resize', () => {
handleResize();
});
}
}, []);
useEffect(() => {
if (width !== undefined) {
const handleValues = () => {
Object.keys(breakpoints).forEach((breakpoint, index) => {
setValues((prev) => ({
...prev,
[breakpoint]: width >= Object.values(breakpoints)[index],
}));
});
};
handleValues();
return () => window.removeEventListener('resize', handleValues);
}
}, [width]);
const exposed = {
width,
values,
};
return <BreakpointsContext.Provider value={exposed}>{children}</BreakpointsContext.Provider>;
};
export const useBreakpoints = () => useContext(BreakpointsContext);
export default BreakpointsProvider;
_app.tsx
// Providers
import { ThemeProvider } from 'next-themes';
import BreakpointsProvider from '../hooks/useBreakpoints';
// Variables
import { Style } from '#master/css';
// Styles
import '#master/normal.css';
import '../styles/master.css';
// Types
import { ReactElement, ReactNode } from 'react';
import type { NextPage } from 'next';
import type { AppProps } from 'next/app';
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode;
};
interface AppPropsWithLayout extends AppProps {
Component: NextPageWithLayout;
}
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => page);
return getLayout(
<ThemeProvider themes={Style.colorSchemes} attribute="class">
<BreakpointsProvider breakpoints={Style.breakpoints}>
<Component {...pageProps} />
</BreakpointsProvider>
</ThemeProvider>
);
}
export default MyApp;
Then I call it inside of a component like this
const test = useBreakpoints();
console.log(test);
I am working with NextJS, if that changes anything.

Pass useRef() to grandchild component using typescript

Example code can be found below...
(Parent) App.tsx
import React, { useRef } from 'react';
import { Switch } from "react-router-dom";
import axios, { AxiosResponse } from 'axios';
import { AdminRoute } from './auth/protectedRoutes';
import Home from './views/Home';
export interface Data {
id: number;
name: string;
}
function App(): JSX.Element {
// variables
const searchDebouncerRef = useRef(false);
const [data, setData] = useRef<Array<Data>>([]);
// functions
async function updateData(searchString?: string | null) {
try {
const response: AxiosResponse<Array<Data>> = searchString
? await axios.get(`https://example.com/Api/Data$filter=contains(name, ${searchString})`)
: await axios.get('https://example.com/Api/Data');
if (searchDebouncerRef.current) {
return;
}
setData(response.data);
catch (e) {
console.log(e);
}
}
async function initData() {
try {
await updateData();
catch (e) {
console.log(e);
}
}
// setup
useEffect(() => {
initData();
}, []);
// render
return (
<>
<Switch>
<AdminRoute path="/">
<Home ref={searchDebouncerRef} updateData={updateData} data={data} />
</AdminRoute>
</Switch>
</>
)
}
export App;
(Child) Home.tsx
import React, { forwardRef } from 'react';
import { Data as DataRow } from '../App';
import Search from '../components/Search';
interface PROPS {
updateData: (searchString?: string | null) => void;
data: Array<DataRow>;
}
const Home: React.FC<any> = forwardRef(
({ updateData, data }: PROPS, ref) => {
return (
<div>
<Search isSearchDebouncingRef={ref} updateData={updateData} />
{data.map((row: DataRow) => ({
<p key={row.id}>{row.name}</p>
}))}
</div>
);
}
);
(Grandchild) Search.tsx
import React, { ChangeEvent, useCallback, useState } from 'react';
import { debounce } from 'lodash';
interface PROPS {
updateData: (searchString?: string | null) => void;
isSearchDebouncingRef: ???? // <-----------------------------------------------------
}
function Search({ updateData, isSearchDebouncingRef }: PROPS): JSX.Element {
// variables
const [searchText, setSearchText] = useState('');
const [searchDebouncerHasCompleted, setSearchDebouncerHasCompleted] = useState(false);
const searchDebouncer = useCallback(
debounce(() => {
setSearchDebouncerHasCompleted(true);
isSearchDebouncingRef.current = false;
}, 3000),
[]
);
// functions
function handleSearch(event: ChangeEvent<HTMLInputElement>) {
setSearchText(event.target.value);
isSearchDebouncingRef.current = true;
searchDebouncer();
}
// setup
useEffect(() => {
if (searchDebouncerHasCompleted) {
setSearchDebouncerHasCompleted(false);
updateData(searchText || null);
}
}, [searchDebouncerHasCompleted]);
// render
return <input type="text" value={searchText} onChange={(e) => handleSearch(e)} />; // ToDo: add icons(search, spinner, clear)
}
The grandchild file is where I am having trouble figuring out how to identify the type for the ref(see interface PROPS). Also the child file I would like to replace any, with the proper type if possible.

how to use functional mobX store in react class components?

here is my init store function:
import React, { FC } from 'react';
import { useLocalObservable } from 'mobx-react';
import { TestStore, ITestStore } from './TestStore';
interface IStoreContext {
testStore: ITestStore;
}
export const StoreContext = React.createContext<IStoreContext>({} as IStoreContext);
export const StoreProvider: FC = ({ children }) => {
const testStore = useLocalObservable(TestStore);
const stores = {
testStore,
};
return <StoreContext.Provider value={stores}>{children}</StoreContext.Provider>;
};
export const useRootStore = () => {
const rootStore = React.useContext(StoreContext);
if (!rootStore) {
throw new Error('useStore must be used within a StoreProvider');
}
return rootStore;
};
and this is how i use it:
const { testStore } = useRootStore();
But I can't use the hook inside the class component.
So how get the store inside the class component ?
thanks.
You can create a Higher-Order Component that uses your hook and passes the result to the wrapped class component.
Example
function withRootStore(Component) {
return function WrappedComponent(props) {
const rootStore = useRootStore();
return <Component {...props} rootStore={rootStore} />;
}
}
// ...
class MyComponent extends React.Component {
render() {
const { testStore } = this.props.rootStore;
// ...
}
}
export default withRootStore(MyComponent);

Get movieId from router v6 to component (movieDB) [duplicate]

How can i access url parameter in my react component ?
App.js
<Route path="/question/:id" element={<QuestionView />} />
QuestionView.js
class QuestionView extends React.Component {
render() {
const { questions, users } = this.props;
const {id} = ???
Issue
In react-router-dom v6 the Route components no longer have route props (history, location, and match), and the current solution is to use the React hooks "versions" of these to use within the components being rendered. React hooks can't be used in class components though.
To access the match params with a class component you must either convert to a function component, or roll your own custom withRouter Higher Order Component to inject the "route props" like the withRouter HOC from react-router-dom v5.x did.
Solution
I won't cover converting a class component to function component. Here's an example custom withRouter HOC:
const withRouter = WrappedComponent => props => {
const params = useParams();
// etc... other react-router-dom v6 hooks
return (
<WrappedComponent
{...props}
params={params}
// etc...
/>
);
};
And decorate the component with the new HOC.
export default withRouter(Post);
This will inject a params prop for the class component.
this.props.params.id
HOC withRouter TypeScript version with generic Params
withRouter.tsx
import { ComponentType } from 'react';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
export interface WithRouterProps<T = ReturnType<typeof useParams>> {
history: {
back: () => void;
goBack: () => void;
location: ReturnType<typeof useLocation>;
push: (url: string, state?: any) => void;
}
location: ReturnType<typeof useLocation>;
match: {
params: T;
};
navigate: ReturnType<typeof useNavigate>;
}
export const withRouter = <P extends object>(Component: ComponentType<P>) => {
return (props: Omit<P, keyof WithRouterProps>) => {
const location = useLocation();
const match = { params: useParams() };
const navigate = useNavigate();
const history = {
back: () => navigate(-1),
goBack: () => navigate(-1),
location,
push: (url: string, state?: any) => navigate(url, { state }),
replace: (url: string, state?: any) => navigate(url, {
replace: true,
state
})
};
return (
<Component
history={history}
location={location}
match={match}
navigate={navigate}
{...props as P}
/>
);
};
};
MyClass.tsx
import { Component } from 'react';
import { withRouter, WithRouterProps } from './withRouter';
interface Params {
id: string;
}
type Props = WithRouterProps<Params>;
class MyClass extends Component<Props> {
render() {
const { match } = this.props;
console.log(match.params.id); // with autocomplete
return <div>MyClass</div>;
}
}
export default withRouter(MyClass);
Here's the code example I'm using in my project to get the id from the URL:
import React from 'react'
import {Button} from 'antd'
import {useParams} from 'react-router-dom'
const DeleteUser = () => {
const {id} = useParams()
const handleDelete = async () => {
// handle delete function
}
return (
<Button onClick={handleDelete}>Delete User</Button>
)
}
export default DeleteUser
If you would like to use a class, then you will need to wrap it with the withRouter. I provide an example below:
This is my class for the movie form:
class MovieForm extends Form {
state = {
data: {
title: "",
genreId: "",
numberInStock: "",
dailyRentalRate: ""
},
genres: [],
errors: {}
};
schema = {
_id: Joi.string(),
title: Joi.string()
.required()
.label("Title"),
genreId: Joi.string()
.required()
.label("Genre"),
numberInStock: Joi.number()
.required()
.min(0)
.max(100)
.label("Number in Stock"),
dailyRentalRate: Joi.number()
.required()
.min(0)
.max(10)
.label("Daily Rental Rate")
};
componentDidMount() {
const genres = getGenres();
this.setState({ genres });
// const movieId = this.props.match.params.id;
const movieId = this.props.params.id;
if (movieId === "new") return;
const movie = getMovie(movieId);
if (!movie) return this.props.history.replace("/not-found");
this.setState({ data: this.mapToViewModel(movie) });
}
mapToViewModel(movie) {
return {
_id: movie._id,
title: movie.title,
genreId: movie.genre._id,
numberInStock: movie.numberInStock,
dailyRentalRate: movie.dailyRentalRate
};
}
doSubmit = () => {
saveMovie(this.state.data);
this.props.navigate("/movies");
};
render() {
return (
<div>
<h1>Movie Form</h1>
<form onSubmit={this.handleSubmit}>
{this.renderInput("title", "Title")}
{this.renderSelect("genreId", "Genre", this.state.genres)}
{this.renderInput("numberInStock", "Number in Stock", "number")}
{this.renderInput("dailyRentalRate", "Rate")}
{this.renderButton("Save")}
</form>
</div>
);
}
}
I write a wrapper outside of the class:
const withRouter = WrappedComponent => props => {
const params = useParams();
const navigate = useNavigate();
return (
<WrappedComponent
{...props}
params={params}
navigate={navigate}
/>
);
};
Now, at the end of the file I will export it like below:
export default withRouter(MovieForm);
Insdie the withRouter, I get all the functions that I will use later inside the class:
const params = useParams();
const navigate = useNavigate();
TypeScript version
withRouter.tsx
import React from 'react';
import {
useLocation,
useNavigate,
useParams,
NavigateFunction,
Params,
Location,
} from 'react-router-dom';
export interface RouterProps {
router: {
navigate: NavigateFunction;
readonly params: Params<string>;
location: Location;
}
}
function withRouter(Component: React.ComponentType<RouterProps>) {
const ComponentWithRouterProp: React.FC = () => {
const location = useLocation();
const navigate = useNavigate();
const params = useParams();
return (
<Component
router={{ location, navigate, params }}
/>
);
};
return ComponentWithRouterProp;
}
export default withRouter;
MyComponent.tsx
import React from 'react';
import { connect } from 'react-redux';
import { RootState } from '##/redux/store';
import {
addSettings,
updateSettings,
} from '##/redux/mySlice';
import withRouter, { RouterProps } from '##/withRouter';
const mapState = (state: RootState) => ({
myStore: state.variation.myStore,
});
const mapDispatch = {
addSettings,
updateSettings,
};
type IProps = ReturnType<typeof mapState> & typeof mapDispatch & RouterProps;
class MyComponent extends React.Component<IProps> {
constructor(props: IProps) {
super(props);
}
onNavigateHome = () => {
this.props.router.navigate('/');
}
render(): React.ReactNode {
return (
<div className="test" onClick={this.onNavigateHome}>test</div>
);
}
}
export default withRouter(connect(mapState, mapDispatch)(MyComponent));
I had a similar issue, described here:
Params from React Router with class components and typescript
I created a new functional component, so I can use useParams():
import React from 'react';
import { useParams } from 'react-router';
type Props = {
children: JSX.Element
};
export const WithParams: React.FC<Props> = (props) => {
const params = useParams();
return React.cloneElement(props.children, {...props.children.props, ...params });
};
and added it to my Route.element
<Route path="/Contacts/VerifyEmailAddress/:id"
element={
<WithParams>
<VerifyEmail />
</WithParams>
}>
</Route>
and added the parameters I need to the props of my child component.
export class VerifyEmailProps {
public id?: string;
}

Resources