I have a simple HOC which injects a react context as a prop in the wrappedcomponent.
function withTranslate(WrappedComponent) {
//we forward the ref so it can be used by other
return React.forwardRef((props, ref) => (
<TranslatorContext.Consumer>
{context => (<WrappedComponent {...props} translate={context} ref={ref} />)}
</TranslatorContext.Consumer>)
)
}
Now I want a secondary HOC which uses the same context, but changes some predefined props using this context. I succeed with following code:
export function withTranslatedProps(WrappedComponent,propsToBeTransLated) {
//propsToBetranslated is array with all props which will be given via keys
const translateProps=(translate,props)=>{
const ownProps=Object.assign({},props)
propsToBeTransLated.forEach(p=>{
if(ownProps.hasOwnProperty(p)){
ownProps[p]=translate(ownProps[p])
}
})
return ownProps
}
return React.forwardRef((props, ref) => {
console.log("render contextconsumer")
return (
<TranslatorContext.Consumer>
{context => (
<WrappedComponent {...translateProps(context,props)} ref={ref} />
)}
</TranslatorContext.Consumer>)
})
}
But I almost exactly use the same HOC as withTranslate. Is there a better option (without repeating myself) to do this?
edit
I think i solved it:
const _translateProps=(propsToBeTransLated,translate,props)=>{
const ownProps=Object.assign({},props)
propsToBeTransLated.forEach(p=>{
if(ownProps.hasOwnProperty(p)){
ownProps[p]=translate(ownProps[p])
}
})
return ownProps
}
export function withTranslatedProps(WrappedComponent,propsToBeTransLated) {
//propsToBetranslated is array with all props which will be given via keys
let retrieveProps=propsToBeTransLated?_translateProps.bind(null,propsToBeTransLated):(context,props)=>({translate:context,...props})
return React.forwardRef((props, ref) => {
console.log("render contextconsumer")
return (
<TranslatorContext.Consumer>
{context => (
<WrappedComponent {...retrieveProps(context,props)} ref={ref} />
)}
</TranslatorContext.Consumer>)
})
}
Anyone with other possibly better solutions?
You can reuse withTranslate HOC or use the same HOC adding options.
Reusing withTranslate HOC:
/* function that translate the propsToBeTransLated */
const translateProps = (propsToBeTransLated, translate, props) =>
propsToBeTransLated.reduce((translatedProps, prop) => {
if(props.hasOwnProperty(prop))
translatedProps[prop] = translate(props[prop]);
return translatedProps;
}, {});
export function withTranslatedProps(WrappedComponent, propsToBeTransLated = []) {
// HOC inside HOC
const addTranslationsToProps = WrappedComponentWithContext =>
React.forwardRef((props, ref) => (
<WrappedComponentWithContext
{...props}
{...translateProps(propsToBeTransLated, props.translate, props)}
ref={ref}
/>
)
);
// first call withTranslate to add the context
return addTranslationsToProps(withTranslate(WrappedComponent));
}
Adding options to withTranslate HOC
const translateProps = (propsToBeTransLated, translate, props) =>
propsToBeTransLated.reduce((translatedProps, prop) => {
if(props.hasOwnProperty(prop))
translatedProps[prop] = translate(props[prop]);
return translatedProps;
}, {});
export function withTranslate(WrappedComponent, options) {
const { propsToBeTransLated = [] } = options;
return React.forwardRef((props, ref) => (
<TranslatorContext.Consumer>
{context => (
<WrappedComponent
{...props}
{...translateProps(propsToBeTransLated, context, props)}
translate={context}
ref={ref}
/>
)}
</TranslatorContext.Consumer>
));
}
Related
I'm trying to pass a ref to a component with a similar approach to the following code block, but the current value always returns undefined. This approach works fine with a plain FlatList from react-native, however it doesn't work once I'm using either an Animated.FlatList or an Animated.createAnimatedComponent(FlatList) :
const Parent = () => {
const flatListRef = useRef();
useEffect(() => {
console.log(flatListRef.current) // undefined
})
return (
<View>
<Child ref={flatListRef} />
</View>
)
}
const Child = React.forwardRef((props, ref) => {
return (
<Animated.FlatList
ref={ref}
/>
)
})
The library react-native-reanimated works a little bit different in comparison to react-native-animated.
If we create the animated component via Animated.createAnimatedComponent(FlatList), then everything works as expected.
Here is a working version of your code. I have logged the function scrollToIndex of the FlatList ref for testing purposes.
import Animated from "react-native-reanimated"
const ReanimatedFlatList = Animated.createAnimatedComponent(FlatList);
const Parent = (props) => {
const flatListRef = useRef(null);
useEffect(() => {
console.log(flatListRef.current.scrollToIndex)
}, [])
return (
<View>
<Child ref={flatListRef} />
</View>
)
}
const Child = React.forwardRef((props, ref) => {
return (
<ReanimatedFlatList
ref={ref}
/>
)
})
On my follow up question from here : How to pass data from child to parent component using react hooks
I have another issue.
Below is the component structure
export const Parent: React.FC<Props> = (props) => {
const [disabled, setDisabled] = React.useState(false);
const createContent = (): JSX.Element => {
return (
<Authorization>
{<ErrorPanel message={errorMessage} setDisabled={setDisabled}/>}
<MyChildComponent/>
</<Authorization>
);
}
return (
<Button onClick={onSubmit} disabled={disabled}>My Button</Button>
{createContent()}
);
};
const Authorization: React.FC<Props> = (props) => {
const [disabled, setDisabled] = React.useState(false);
const render = (errorMessage : JSX.Element): JSX.Element => {
return (
<>
{<ErrorPanel message={errorMessage} setDisabled={setDisabled}/>}
</>
);
};
return (
<>
<PageLoader
queryResult={apiQuery}
renderPage={render}
/>
{props.children}
</>
);
};
How do I pass the disabled state value from Authorization component to my child which is invoked by
{props.children}
I tried React.cloneElement & React.createContext but I'm not able to get the value disabled to the MyChildComponent. I could see the value for disabled as true once the errorMessage is set through the ErrorPanel in the Authorization component.
Do I need to have React.useEffect in the Authorization Component?
What am I missing here?
You need to use React.Children API with React.cloneElement:
const Authorization = ({ children }) => {
const [disabled, setDisabled] = React.useState(false);
const render = (errorMessage) => {
return (
<>{<ErrorPanel message={errorMessage} setDisabled={setDisabled} />}</>
);
};
return (
<>
<PageLoader queryResult={apiQuery} renderPage={render} />
{React.Children.map(children, (child) =>
React.cloneElement(child, { disabled })
)}
</>
);
};
// |
// v
// It will inject `disabled` prop to every component's child:
<>
<ErrorPanel
disabled={disabled}
message={errorMessage}
setDisabled={setDisabled}
/>
<MyChildComponent disabled={disabled} />
</>
You can make use of React.cloneElement to React.Children.map to pass on the disabled prop to the immediate children components
const Authorization: React.FC<Props> = (props) => {
const [disabled, setDisabled] = React.useState(false);
const render = (errorMessage : JSX.Element): JSX.Element => {
return (
<>
{<ErrorPanel message={errorMessage} setDisabled={setDisabled}/>}
</>
);
};
return (
<>
<PageLoader
queryResult={apiQuery}
renderPage={render}
/>
{React.Children.map(props.children, child => {
return React.cloneElement(child, { disabled })
})}
</>
);
};
UPDATE:
Since you wish to update the parent state to, you should store the state and parent and update it there itself, instead of storing the state in child component too.
export const Parent: React.FC<Props> = (props) => {
const [disabled, setDisabled] = React.useState(false);
const createContent = (): JSX.Element => {
return (
<Authorization setDisabled={setDisabled}>
{<ErrorPanel message={errorMessage} disabled={disabled} setDisabled={setDisabled}/>}
<MyChildComponent disabled={disabled}/>
</<Authorization>
);
}
return (
<Button onClick={onSubmit} disabled={disabled}>My Button</Button>
{createContent()}
);
};
const Authorization: React.FC<Props> = (props) => {
const render = (errorMessage : JSX.Element): JSX.Element => {
return (
<>
{<ErrorPanel message={errorMessage} disabled={props.disabled} setDisabled={props.setDisabled}/>}
</>
);
};
return (
<>
<PageLoader
queryResult={apiQuery}
renderPage={render}
/>
{props.children}
</>
);
};
I want to make a page with react components.
For that, I want to separate my code in several parts.
I want to create a page in my Layout function that uses various components like my MenuComponent.
However, I do not understand how to recover my MenuComponent in my Layout.
Thanks for your help.
function Menu () {
const [menuItems, setItems] = useState(null);
useEffect(() => {
getMenuItems().then(setItems);
}, []);
if (!menuItems) {
return null;
}
return (
<div>
{menuItems.data.root.children.map(menu => {
return (
<TreeItem key={menu.id} nodeId="1" label={menu.text} labelIcon={Label}>
{menu.children.map(child => {
return (
<TreeItem key={child.id} nodeId="1" label={child.text} labelIcon={Label}>
{console.log(child.text)}
</TreeItem>
);
})}
</TreeItem>
);
})}
</div>
);
}
export default function Layout() {
const classes = useStyles();
if (!isLogged()) {
return (
<div className="fondLogin" fullscreen>
<SnackbarProvider
maxSnack={1}
anchorOrigin={{
vertical: "top",
horizontal: "center"
}}
>
<Login onSuccess />
</SnackbarProvider>
</div>
);
}
return (
<div className="containerGeneral">
<InactivityManager />
<Menu />
</div>
);
}
Satif is completely right, if you are using a hooks approach, then you should make an async calls into useEffect hook.
Another thing, your component should always return something. If you want to prevent render just return null. In your case you are returning undefined.
function Menu () {
const [menuItems, setItems] = useState(null);
useEffect(() => {
getMenuItems().then(setItems);
}, []);
if (!menuItems) {
return null;
}
return (
<div>
{menuItems.children.map(menu => {
return (
<TreeItem key={menu.id} nodeId="1" label={menu.text} labelIcon={Label}>
</TreeItem>
);
})};
</div>
);
}
The menu component is wrong. You need to fetch a data in useEffect or componentDidMount methods.
There is an existing code:
const FooRoute: React.SFC<RouteProps> =
({ component: Component, ...rest }) => {
if (!auth.isFoo()) {
return <Redirect to={{ pathname: '/404' }} />;
}
if ('render' in rest) {
return (
<Route {...rest} render={rest.render} />
);
} else {
return (
Component
?
<Route
{...rest}
render={
(props: RouteComponentProps<{}>) => <Component {...props} />}
/>
: null
);
}
};
There is an error here:
Now how does the getComponent function will look like in this case?
Thought about smth like:
const getComponent = (props: RouteComponentProps<{}>) => () => (
<Component {...props} />
)
Then one can simply:
<Route
{...rest}
render={getComponent}
/>
But in this case Component is undefined. Any clues?
EDIT: Important Note - using TypeScript. So have to pass the Component somehow down into getComponent.
EDIT2: the reason I've used double lambda is because it allows handling situations like this:
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => (foo: string) => {
...
}
with
<button onClick={handleClick('bar')} ... // foo === 'bar'
In your case getComponent is a lambda function which returns another lambda function but not component because of (props) => () => .... Modify your getComponent function to receive Component from render function like below.
const getComponent = (Component: Component) => (props: RouteComponentProps<{}>) => (
<Component {...props} />
)
and modify this 'render' function too.
<Route
{...rest}
render={getComponent(Component)}
/>
Note: You are receiving component as component with small c and using it as Component with capital C.
I wonder if it is a better way to DRY this code, have you guys any ideas?
The props are the same, just the component change...
render() {
const { input: { value, onChange }, callback, async, ...rest } = this.props;
if (async) {
return (
<Select.Async
onChange={(val) => {
onChange(val);
callback(val);
}}
value={value}
{...rest}
/>
);
}
return (
<Select
onChange={(val) => {
onChange(val);
callback(val);
}}
value={value}
{...rest}
/>
);
}
With:
let createElement = function(Component) {
return (
<Component onChange={(val) => {
onChange(val);
callback(val);
}}
value={value}
{...rest}
/>
);
};
you can do
let selectAsync = createElement(Select.Async);
let select = createElement(Select);
You can render them in the jsx part with {{select}} and {{selectAsync}}
P.S.: I didnt test this directly, but did something very similar a few days ago, so this approach should work. Note that Component must start with a capital letter.