I am trying to create a HOC using react functional component that will take a component and some props, but I think I am missing something I did not get the props value in the component which I passed. I am also using typescript
My higher-order component:
interface EditChannelInfo {
Component: any;
setIsCollapsed: Function;
isCollapsed: boolean;
}
const EditChannelInfo = (props: EditChannelInfo): ReactElement => {
const {isCollapsed, setIsCollapsed, Component} = props;
const {data: gamesList} = useGamesList();
const games = gamesList.games.map((list: GamesList) => ({
value: list.gameId,
label: list.gameName,
}));
return <Component {...props} />;
};
export default EditChannelInfo;
From here I am passing the component to the higher-order component
import EditChannelInfoWrapper from '../EditChannelInfoWrapper';
const Dashboard: NextPage = (): ReactElement => {
const [isCollapsed, setIsCollapsed] = useState<boolean>(false);
return (
<div>
<EditChannelInfo
Component={EditChannelInfoWrapper}
setIsCollapsed={setIsCollapsed}
isCollapsed={isCollapsed}
/>
</div>
);
};
export default Dashboard;
I am getting games undefined
interface EditChannelInfoWrapper {
games: any;
}
const EditChannelInfoWrapper = (
props: EditChannelInfoWrapper,
): ReactElement => {
const {
games,
} = props;
console.log(games);
return ()
}
It looks like you're not passing your games prop to the Component here: <Component {...props} />.
Add in your games prop and it should work as expected <Component {...props} games={games} />
Related
I have a react HoC where I have define few states and I am passing that to wrapped component. But the wrapped component itself has some props.
HoC.tsx
const HOC = (Component: React.ComponentType<T>) => {
const [someState, setSomeState] = useState()
const WrappedComponent = (props: T) =>
return(
<Component {(...props) as T} someState={someState}/>
)
return WrappedComponent
}
Hoc Usage in a component which needs other props
interfae NewComponentProps {
x: number
}
const NewComponent: React.FC<NewComponentProps> = (props) => {
let {x, someState} = props
//Here I am not able to access someState prop which is coming from HOC, typescript is giving error
return ( ... )
}
export default HoC(NewComponent)
How to handle such case and if I add someState in NewComponentProps interface it will work but I have to pass someState prop when I will call the NewComponent anywhere
So what should be the props type of new component to access both props??
Here is a small example on how you might type it to make it work
type WrappedProps = {
b: string;
};
// Here you type the child component as generic T combined with
// your Wrapped props
const Wrapped = <T,>(Comp: ComponentType<T & WrappedProps>) => {
return (props: T) => {
return <Comp {...props} b="World" />;
};
};
// ============================
type CompProps = {
a: string;
};
// You need to manually pass the props of your component to the Wrapped
// function, because it can't infer the type
const Comp = Wrapped<CompProps>((props) => {
// props now has the type CompProps & WrappedProps
return (
<p>
A: {props.a}
<br />
B: {props.b}
</p>
);
});
// ============================
export default function App() {
return <Comp a="Hello" />;
}
I have a requirement to render a specific component based on params. I have two solutions in mind.
Solution 1:
export const getForm = () => {
const { name } = useParams();
const ComponentMap = {
account: <Account />,
contact: <Contact />,
};
return ComponentMap[name];
};
Solution 2:
export const getForm = () => {
const { name } = useParams();
const ComponentMap = {
account: Account,
contact: Contact,
};
return <ComponentMap[name] />;
};
I am more inclined to solution 2 as it won't create JSX elements for all the map values.
What's the standard pattern in React world. Can someone help me in deciding an approach?
Basically what you would do is after you extract a property "name" from useParams, my suggestion would be that you pass it as a Props to the component.
Like this
export const getForm = () => {
const { name } = useParams();
const componentForm = {
account: <Account name={name} />,
contact: <Contact name={name} />,
};
return componentForm[name];
};
Then I would capture that in the componenet:
For example in Account.jsx
export const Account = ({name}) => {
getDetails(name)
}
At least that is what I would do using React and React Routers
Solution 1 is a common practice, Solution 2 looks weird. Not sure about pros and cons, but Solution 1 is how 99% of React devs do, IMHO.
My personal preference is for Solution #2. This is a very powerful pattern as it allows you to pass through a standard set of props to any of your render components. For example, perhaps we have other values in the params that we want to pass as props. You could get props from other sources like redux, firebase, etc.
export const DynamicForm = () => {
const { name, ...rest } = useParams();
const ComponentMap = {
account: Account,
contact: Contact,
};
const Component = ComponentMap[name];
return <Component {...rest} />;
};
With Typescript types:
import { useParams } from "react-router-dom";
import React from "react";
const Account = ({someProp}: {someProp: string}) => <div>{someProp}</div>
const Contact = ({otherProp}: {otherProp: string}) => <div>{otherProp}</div>
interface MyParamsType {
name: string;
someProp: string;
otherProp: string;
}
export const DynamicForm = () => {
const { name, ...rest } = useParams<MyParamsType>();
// define the components' props as the type of `rest`
const ComponentMap: Record<string, React.ComponentType<Omit<MyParamsType, 'name'>>> = {
account: Account,
contact: Contact,
};
const Component = ComponentMap[name];
return <Component {...rest} />;
};
Typescript Playground Link
I am testing a component that doesn't have props but it expects to be fulfilled with data coming from context.
Let's say this is my component:
export const MyComponent: FC = () => {
const { arrayOfObjects } = useFn()
return arrayOfObjects.length ? arrayOfObjects.map((q: VariableConfig, i: number) => (
<SelectedQuestionTile
{...{
key: i + 1,
question: q,
questionIdx: i,
}}
/>
)) : <p>No Data</p>
}
This is the only test I have so far:
import React from 'react'
import { render, screen } from '#testing-library/react'
import { MyComponent } from './MyComponent'
describe('MyComponent', () => {
test('It renders with empty containers', () => {
render(<MyComponent />)
expect(screen.getByText("No Data")).toBeInTheDocument()
})
})
There, I am testing the component on its initial state which renders a couple of empty containers since they don't have any data yet. The data is present here on the line const { arrayOfObjects } = useFn(). The arrayOfObjects is just hardcoded data, not dynamic.
What am I missing?
Test it by wrapping it in an actual context provider.
render(
<MyContext.Provider value={{ arryOfObjects }}>
<MyComponent />
<MyContext.Provider>
)
I am attempting to migrate from a class based component to functional component. It is a connected component using mapState.
This is what I had:
import { connect } from 'react-redux'
import { fetchArticles } from '../shared/actions/articleActions';
import { AppState } from '../shared/types/genericTypes';
import Article from '../shared/models/Article.model';
type Props = {
articles?: Article[]
fetchArticles?: any,
};
const mapState = (state: AppState, props) => ({
articles: state.articleReducers.articles,
...props
});
const actionCreators = {
fetchArticles,
};
class NewsArticles extends Component<Props> {
componentDidMount() {
if (!this.props.articles || !this.props.articles.length) {
this.props.fetchArticles();
}
}
render() {
return (...);
}
}
export default connect(mapState, actionCreators)(NewsArticles)
Here is what I have now:
// same imports except for FC and useEffec from react.
type Props = {
articles?: Article[];
fetchArticles?: any;
};
const mapState = (state: AppState, props: Props) => ({
articles: state.articleReducers.articles,
...props,
});
const actionCreators = {
fetchArticles,
};
const NewsArticles: FC<Props> = ({ articles, fetchArticles }) => {
useEffect(() => {
if (!articles || !articles.length) {
fetchArticles();
}
}, []);
return (...);
};
export default connect(mapState, actionCreators)(NewsArticles);
The main concern I have is the props.
Before they were like this
const mapState = (state: AppState, props) => ({
articles: state.articleReducers.articles,
...props
});
And used like this:
componentDidMount() {
if (!this.props.articles || !this.props.articles.length) {
this.props.fetchArticles();
}
}
Now that I have a functional component, I am getting this
const mapState = (state: AppState, props: Props) => ({
articles: state.articleReducers.articles,
...props,
});
And used like this:
useEffect(() => {
if (!articles || !articles.length) {
fetchArticles();
}
}, []);
So how props will work now that articles and fetchArticles are not called like this.props.articles and only articles so, does make any sense to spread props …props on mapState?
There's no need to spread props in mapState.
This:
const mapState = (state: AppState, props: Props) => ({
articles: state.articleReducers.articles,
...props,
});
is equivalent to this:
const mapState = (state: AppState) => ({
articles: state.articleReducers.articles,
});
Any extra props will get passed into your component in addition to the props from mapState and mapDispatch (which you've called actionCreators).
Your code is compilable but is not fully correct with connect function signature. Lets look on signature
<TStateProps = {}, TDispatchProps = {}, TOwnProps = {}, State = {}>(
mapStateToProps: MapStateToPropsParam<TStateProps, TOwnProps, State>,
mapDispatchToProps: MapDispatchToPropsNonObject<TDispatchProps, TOwnProps>
): InferableComponentEnhancerWithProps<TStateProps & TDispatchProps, TOwnProps>;
It has 4 types,
TSTateProps is type of props, derived from Redux state. In your sample they are
type StateProps = {
articles: Article[];
}
Only articles are derived from Redux state.
TDispatchProps is type of props containing actions your component will dispatch. As you passing actionCreators object to connect TDispatchProps should equal to typeof actionCreators (we should get type of object actionCreators).
TOwnProps is type of props your component get from parent (not from Redux). You not use props from parent, so TOwnProps will equal to {}.
TState is type of state in Redux. It is AppState.
So to be fully correct with Redux, you should do
type StateProps = {
articles: Article[];
};
const mapState = (state: AppState): StateProps => ({
articles: state.articleReducers.articles
});
const actionCreators = {
fetchArticles,
};
type Props = StateProps & typeof actionCreators; // These is correct props for your component
const NewsArticles: FC<Props> = ({ articles, fetchArticles }) => {
And in case you'll later add props from parent, just intersect them with Props.
type Props = StateProps & typeof actionCreators & OwnProps;
Your code worked as you added ...props to mapState. And props contained member fetchAction. So you finally got the same Props like I showed in my answer, but slightly incorrect.
I have updated this with an update at the bottom
Is there a way to maintain a monolithic root state (like Redux) with multiple Context API Consumers working on their own part of their Provider value without triggering a re-render on every isolated change?
Having already read through this related question and tried some variations to test out some of the insights provided there, I am still confused about how to avoid re-renders.
Complete code is below and online here: https://codesandbox.io/s/504qzw02nl
The issue is that according to devtools, every component sees an "update" (a re-render), even though SectionB is the only component that sees any render changes and even though b is the only part of the state tree that changes. I've tried this with functional components and with PureComponent and see the same render thrashing.
Because nothing is being passed as props (at the component level) I can't see how to detect or prevent this. In this case, I am passing the entire app state into the provider, but I've also tried passing in fragments of the state tree and see the same problem. Clearly, I am doing something very wrong.
import React, { Component, createContext } from 'react';
const defaultState = {
a: { x: 1, y: 2, z: 3 },
b: { x: 4, y: 5, z: 6 },
incrementBX: () => { }
};
let Context = createContext(defaultState);
class App extends Component {
constructor(...args) {
super(...args);
this.state = {
...defaultState,
incrementBX: this.incrementBX.bind(this)
}
}
incrementBX() {
let { b } = this.state;
let newB = { ...b, x: b.x + 1 };
this.setState({ b: newB });
}
render() {
return (
<Context.Provider value={this.state}>
<SectionA />
<SectionB />
<SectionC />
</Context.Provider>
);
}
}
export default App;
class SectionA extends Component {
render() {
return (<Context.Consumer>{
({ a }) => <div>{a.x}</div>
}</Context.Consumer>);
}
}
class SectionB extends Component {
render() {
return (<Context.Consumer>{
({ b }) => <div>{b.x}</div>
}</Context.Consumer>);
}
}
class SectionC extends Component {
render() {
return (<Context.Consumer>{
({ incrementBX }) => <button onClick={incrementBX}>Increment a x</button>
}</Context.Consumer>);
}
}
Edit: I understand that there may be a bug in the way react-devtools detects or displays re-renders. I've expanded on my code above in a way that displays the problem. I now cannot tell if what I am doing is actually causing re-renders or not. Based on what I've read from Dan Abramov, I think I'm using Provider and Consumer correctly, but I cannot definitively tell if that's true. I welcome any insights.
There are some ways to avoid re-renders, also make your state management "redux-like". I will show you how I've been doing, it far from being a redux, because redux offer so many functionalities that aren't so trivial to implement, like the ability to dispatch actions to any reducer from any actions or the combineReducers and so many others.
Create your reducer
export const initialState = {
...
};
export const reducer = (state, action) => {
...
};
Create your ContextProvider component
export const AppContext = React.createContext({someDefaultValue})
export function ContextProvider(props) {
const [state, dispatch] = useReducer(reducer, initialState)
const context = {
someValue: state.someValue,
someOtherValue: state.someOtherValue,
setSomeValue: input => dispatch('something'),
}
return (
<AppContext.Provider value={context}>
{props.children}
</AppContext.Provider>
);
}
Use your ContextProvider at top level of your App, or where you want it
function App(props) {
...
return(
<AppContext>
...
</AppContext>
)
}
Write components as pure functional component
This way they will only re-render when those specific dependencies update with new values
const MyComponent = React.memo(({
somePropFromContext,
setSomePropFromContext,
otherPropFromContext,
someRegularPropNotFromContext,
}) => {
... // regular component logic
return(
... // regular component return
)
});
Have a function to select props from context (like redux map...)
function select(){
const { someValue, otherValue, setSomeValue } = useContext(AppContext);
return {
somePropFromContext: someValue,
setSomePropFromContext: setSomeValue,
otherPropFromContext: otherValue,
}
}
Write a connectToContext HOC
function connectToContext(WrappedComponent, select){
return function(props){
const selectors = select();
return <WrappedComponent {...selectors} {...props}/>
}
}
Put it all together
import connectToContext from ...
import AppContext from ...
const MyComponent = React.memo(...
...
)
function select(){
...
}
export default connectToContext(MyComponent, select)
Usage
<MyComponent someRegularPropNotFromContext={something} />
//inside MyComponent:
...
<button onClick={input => setSomeValueFromContext(input)}>...
...
Demo that I did on other StackOverflow question
Demo on codesandbox
The re-render avoided
MyComponent will re-render only if the specifics props from context updates with a new value, else it will stay there.
The code inside select will run every time any value from context updates, but it does nothing and is cheap.
Other solutions
I suggest check this out Preventing rerenders with React.memo and useContext hook.
I made a proof of concept on how to benefit from React.Context, but avoid re-rendering children that consume the context object. The solution makes use of React.useRef and CustomEvent. Whenever you change count or lang, only the component consuming the specific proprety gets updated.
Check it out below, or try the CodeSandbox
index.tsx
import * as React from 'react'
import {render} from 'react-dom'
import {CountProvider, useDispatch, useState} from './count-context'
function useConsume(prop: 'lang' | 'count') {
const contextState = useState()
const [state, setState] = React.useState(contextState[prop])
const listener = (e: CustomEvent) => {
if (e.detail && prop in e.detail) {
setState(e.detail[prop])
}
}
React.useEffect(() => {
document.addEventListener('update', listener)
return () => {
document.removeEventListener('update', listener)
}
}, [state])
return state
}
function CountDisplay() {
const count = useConsume('count')
console.log('CountDisplay()', count)
return (
<div>
{`The current count is ${count}`}
<br />
</div>
)
}
function LangDisplay() {
const lang = useConsume('lang')
console.log('LangDisplay()', lang)
return <div>{`The lang count is ${lang}`}</div>
}
function Counter() {
const dispatch = useDispatch()
return (
<button onClick={() => dispatch({type: 'increment'})}>
Increment count
</button>
)
}
function ChangeLang() {
const dispatch = useDispatch()
return <button onClick={() => dispatch({type: 'switch'})}>Switch</button>
}
function App() {
return (
<CountProvider>
<CountDisplay />
<LangDisplay />
<Counter />
<ChangeLang />
</CountProvider>
)
}
const rootElement = document.getElementById('root')
render(<App />, rootElement)
count-context.tsx
import * as React from 'react'
type Action = {type: 'increment'} | {type: 'decrement'} | {type: 'switch'}
type Dispatch = (action: Action) => void
type State = {count: number; lang: string}
type CountProviderProps = {children: React.ReactNode}
const CountStateContext = React.createContext<State | undefined>(undefined)
const CountDispatchContext = React.createContext<Dispatch | undefined>(
undefined,
)
function countReducer(state: State, action: Action) {
switch (action.type) {
case 'increment': {
return {...state, count: state.count + 1}
}
case 'switch': {
return {...state, lang: state.lang === 'en' ? 'ro' : 'en'}
}
default: {
throw new Error(`Unhandled action type: ${action.type}`)
}
}
}
function CountProvider({children}: CountProviderProps) {
const [state, dispatch] = React.useReducer(countReducer, {
count: 0,
lang: 'en',
})
const stateRef = React.useRef(state)
React.useEffect(() => {
const customEvent = new CustomEvent('update', {
detail: {count: state.count},
})
document.dispatchEvent(customEvent)
}, [state.count])
React.useEffect(() => {
const customEvent = new CustomEvent('update', {
detail: {lang: state.lang},
})
document.dispatchEvent(customEvent)
}, [state.lang])
return (
<CountStateContext.Provider value={stateRef.current}>
<CountDispatchContext.Provider value={dispatch}>
{children}
</CountDispatchContext.Provider>
</CountStateContext.Provider>
)
}
function useState() {
const context = React.useContext(CountStateContext)
if (context === undefined) {
throw new Error('useCount must be used within a CountProvider')
}
return context
}
function useDispatch() {
const context = React.useContext(CountDispatchContext)
if (context === undefined) {
throw new Error('useDispatch must be used within a AccountProvider')
}
return context
}
export {CountProvider, useState, useDispatch}
To my understanding, the context API is not meant to avoid re-render but is more like Redux. If you wish to avoid re-render, perhaps looks into PureComponent or lifecycle hook shouldComponentUpdate.
Here is a great link to improve performance, you can apply the same to the context API too