How to render React (with Redux) component only when props are received? - javascript

Let us assume we have a statefull React component (configured to work with Redux):
export class SomeComponent extends Component {
state = {
someObject: {}
};
componentWillMount() {
this.props.getNews();
this.props.getFakeNews();
}
render() {
const {
news,
fakeNews
} = this.props;
if(_.isEmpty(news) || _.isEmpty(fakeNews)){
return <div>Loading</div>
}else{
return <div>Here all component stuff</div>
}
}
SomeComponent.propTypes = {
news: PropTypes.array.isRequired,
fakeNews: PropTypes.array.isRequired
};
export const Some = connect(
state => ({
news: newsSelectors.list(state),
fakeNews: fakeNewsSelectors.list(state)
}),
{
getNews,
getFakeNEws
}
)(withStyles(styles)(SomeComponent), withRouter(SomeComponent));
This component will re-render two times during getting news and fake news. In the render method we need to check if both of them are loaded.
Is there any way to trigger render only when all props are loaded?
In a perfect scenario I'd like to have no detailed null/empty check on the set of props. I believe React or Redux should perform this operation on its own as long the prop is configured as required.

You can add a lifecycle method `shouldComponentUpdate(nextProps, nextState).
You can add the following method and it should resolve it for you:
shouldComponentUpdate(nextProps, nextState) {
if (_.isEmpty(nextProps.news) || _.isEmpty(nextProps.fakeNews)) {
return false;
}
return true;
}

You could do something like:
// HOC factory
function ifComponent (predicate, PlaceHolder) {
return Component => class If extends React.Component {
render () {
if (predicate(this.props)) {
return <Component {...this.props} />
}
return <PlaceHolder {...this.props} />
}
}
}
}
// create the customHOC
const whenPropsLoaded = ifComponent(props => props.news && props.fakeNews, Loader);
// compose the two HOCs using the `compose` function in redux (simple function composition)
const News = compose(
connect(getNewsProps),
whenPropsLoaded(DisplayNews)
);
As a side note you may be interested in the recompose utility library bad its branch HOC (docs here). I think this is pretty much what you want as you seem to know about HOCs.

If you want to avoid null and undefined values from redux. You can use Selectors it was very easy to avoid those things.
const newsSelectors = (state) => {
if(!state.list) { *//list is null or undefined*
return [] or {} *//your wish (Proptypes required value)*
}
else {
return state.list
}
}
export { newsSelectors };

I think you can solve the issue if you rewrite the render function as below.
render() {
const {
news,
fakeNews
} = this.props;
return (
{news && fakeNews ?
<div>Here all component stuff</div>
: <div>Loading</div> }
)
}
I hope this helps you.

Related

React-select defaultValues

i have a select menu with defaultValue is null
when i pass props to it , it dosent rerender with the new props as defaultValues
ps : the select is multi
i tried to use component will recieve props and everything that i find but still dosent work
this is my select component :
import React, { useState, useEffect } from "react";
import Select from "react-select";
class SelectMenu extends React.Component {
state = {
defaultValues: [],
};
componentWillReceiveProps(newProps) {
this.setState({ defaultValues: newProps.defaultValue });
}
render() {
return (
<Select
options={this.props.options}
closeMenuOnSelect={this.props.closeMenuOnSelect}
components={this.props.components}
isMulti={this.props.isMulti}
onChange={(e) => this.props.onChange(e, this.props.nameOnState)}
placeholder={this.props.default}
defaultValue={this.state.defaultValues}
/>
);
}
}
export default SelectMenu;
componentWillReceiveProps won't be called during mounting.
React doesn’t call UNSAFE_componentWillReceiveProps() with initial props during mounting. It only calls this method if some of component’s props may update. (https://reactjs.org/docs/react-component.html#unsafe_componentwillreceiveprops)
Also, componentWillReceiveProps is deprecated and will be removed in React 17. Take a look at getDerivedStateFromProps instead, and especially the notes on when you do not need it.
I beleive that in your case using the constructor will be perfectly fine, something like:
class Components extends React.Component {
constructor(props) {
super(props)
this.state = { some_property: props.defaultValue }
}
}
i find a solution for this problem
by using components will recieve props
and setting my state with the comming props
and in the render you need to do condition to render the select menu only if the state.length !== 0
i posted this answer just in case someone face the same problem i know its not the most optimal solution but it works for me
sorry for the previous solution but its not optimal i find a way to make it work
so instead of defaultvalues
you have to make its as value props
and if you want to catch the deleted and added values to your default
this function will help you alot
onChange = (e) => {
if (e === null) {
e = [];
}
this.setState({
equipments: e,
});
let added = e.filter((elm) => !this.state.equipments.includes(elm));
if (added[0]) {
let data = this.state.deletedEquipments.filter(
(elm) => elm !== added[0].label
);
this.setState({
deletedEquipments: data,
});
}
let Equipments = e.map((elm) => elm.label);
let newEquipments = Equipments.filter(
(elm) => !this.state.fixed.includes(elm)
);
this.setState({
newEquipments: newEquipments,
});
let difference = this.state.equipments.filter((elm) => !e.includes(elm));
if (difference.length !== 0) {
if (
!this.state.deletedEquipments.includes(difference[0].label) &&
this.state.fixed.includes(difference[0].label)
) {
this.setState({
deletedEquipments: [
...this.state.deletedEquipments,
difference[0].label,
],
});
}
}
};
constructor(props) {
super(props);
this.state = {
equipments: [],
newEquipments: [],
deletedEquipments: [],
};
}

Child component doesn't rerender but parent component does rerender. How to make child component rerender?

Parent component does rerender upon receiving new props but its child component doesn't rerender. Child components only render for the first time and never rerender nor receive props from the redux store
I'm getting updated data from redux store in Parent component but not in the child components. Child components only receive data from redux store when they render for the first time
My Parent Component Home.js
Object seaFCLJSON look like this
const seaFCLJSON ={"rates": {"sort":"faster", "someOther": "someOtherValues"}};
when the redux store gets updated, seaFCLJSON looks like this
const seaFCLJSON ={"rates": {"sort":"cheaper","someOther": "someOtherValues"}};
class Home extends Component {
state = {
seaFCLJSON: {}
};
componentDidMount = () => {
this.setState({ seaFCLJSON: this.props.seaFCLJSON });
};
componentWillReceiveProps = nextProps => {
if (this.state.seaFCLJSON !== nextProps.seaFCLJSON) {
this.setState({ seaFCLJSON: nextProps.seaFCLJSON });
}
};
render() {
const { seaFCLJSON } = this.props;
return (
<>
{!isEmpty(seaFCLJSON) && seaFCLJSON.rates && seaFCLJSON.rates.fcl ? (
<FCLContainer fclJSON={seaFCLJSON} />
) : null} //it never rerenders upon getting updated data from redux store
<h5>{JSON.stringify(seaFCLJSON.rates && seaFCLJSON.rates.sort)}</h5> //it rerenders everytime upon getting updated data from redux store
</>
);
}
}
const mapStateToProps = state => {
return {
seaFCLJSON: state.route.seaFCLJSON
};
};
export default connect(
mapStateToProps,
actions
)(Home);
isEmpty.js
export const isEmpty = obj => {
return Object.entries(obj).length === 0 && obj.constructor === Object;
};
My Child Component FCLContainer.js
class FCLContainer extends Component {
state = {
seaFCLJSON: {}
};
componentDidMount = () => {
this.setState({ seaFCLJSON: this.props.seaFCLJSON });
};
componentWillReceiveProps = nextProps => {
console.log("outside state value: ", this.state.seaFCLJSON);
if (this.state.seaFCLJSON !== nextProps.seaFCLJSON) {
this.setState({ seaFCLJSON: nextProps.seaFCLJSON });
console.log("inside state value: ", this.state.seaFCLJSON);
}
};
render() {
const { seaFCLJSON } = this.state;
console.log("rendering .. parent props: ", this.props.fclJSON);
console.log("rendering .. redux store props: ", this.props.seaFCLJSON);
return (
<>
<div className="home-result-container">
<div>
<h5>This child component never rerenders :(</h5>
</div>
</div>
</>
);
}
}
const mapStateToProps = state => {
return {
seaFCLJSON: state.route.seaFCLJSON
};
};
export default connect(
mapStateToProps,
actions
)(FCLContainer);
I don't know whether there are problems in Parent component or problems in the child component. componentWillReceiveProps gets invoked in the parent component but not in the child component. Please ignore any missing semi-colon or braces because I have omitted some unnecessary codes.
Edit 1: I just duplicated value from props to state just for debugging purposes.
I will appreciate your help. Thank you.
Edit 2: I was directly changing an object in redux actions. That's the reason CWRP was not getting fired. It was the problem. For more check out my answer below.
componentWillReceiveProps will be deprecated in react 17, use componentDidUpdate instead, which is invoked immediately after updating occurs
Try something like this:
componentDidUpdate(prevProps, prevState) {
if (this.prevProps.seaFCLJSON !== this.props.seaFCLJSON) {
this.setState({ seaFCLJSON: this.props.seaFCLJSON });
}
};
At the first place it is absolutely meaningless to duplicate value from props to state, what is the meaning of it? Totally pointless, just keep it in props
About your issue - most probably this condition doesnt match, thats why child component doesnt trigger
!isEmpty(seaFCLJSON) && seaFCLJSON.rates && seaFCLJSON.rates.fcl
check it in debugger
As far as I can see, your problem is that you pass the following to your child component:
<FCLContainer fclJSON={seaFCLJSON} />
But you assume that you receive a prop called 'seaFCLJSON':
componentDidMount = () => {
this.setState({ seaFCLJSON: this.props.seaFCLJSON });
};
You should change your code to:
<FCLContainer seaFCLJSON={seaFCLJSON} />
Apart from that, as #Paul McLoughlin already mentioned, you should use the prop directly instead of adding it to your state.
I found the issue I was directly mutating the object in actions. I only knew state should not be directly mutated in class or inside reducer. I changed the actions where I was directly changing an object and then saving it in redux store via dispatch and, then I received the updated props in CWRP. This really took me a lot of times to figure out. This kind of issue is hard to find out at least for me. I guess I get this from https://github.com/uberVU/react-guide/issues/17
A lesson I learned: Never directly mutate an Object
I changed this
//FCL sort by faster
export const sortByFasterFCLJSON = () => async (dispatch, getState) => {
let seaFCLJSON = getState().route.seaFCLJSON;
if (!seaFCLJSON.rates) return;
seaFCLJSON.rates.fcl = _.orderBy(
seaFCLJSON.rates.fcl,
["transit_time"],
["asc"]
);
seaFCLJSON.rates.sort = "Faster"; //this is the main culprit
dispatch({ type: SET_SEA_FCL_JSON, payload: seaFCLJSON });
};
to this
//FCL sort by faster
export const sortByFasterFCLJSON = () => async (dispatch, getState) => {
let seaFCLJSON = getState().route.seaFCLJSON;
if (!seaFCLJSON.rates) return;
seaFCLJSON.rates.fcl = _.orderBy(
seaFCLJSON.rates.fcl,
["transit_time"],
["asc"]
);
// seaFCLJSON.rates.sort = "Faster"; //this was the main culprit, got lost
seaFCLJSON = {
...seaFCLJSON,
rates: { ...seaFCLJSON.rates, sort: "Faster" }
};
dispatch({ type: SET_SEA_FCL_JSON, payload: seaFCLJSON });
};
the power of not mutating data
Side note: Redux Troubleshooting

How do you render a component based on an AsyncStorage value in React Native?

I have a component that looks like this:
export default class Class1 extends Component {
render = async () => {
await AsyncStorage.getItem('someValue', (error, someValue) => {
return(
<Class2 prop1={someValue}/>
)
}
}
}
What's happening here is that I need to render Class1 based on the value of someValue that is returned from AsyncStorage. The problem is, you can't make the render() method async because async functions return a promise, and render() needs to return a React component.
Does anyone know how I can do this?
Thanks!
For this kind of tasks, you would put the value in your state. And based on the state, you will render the class as required.
In your componentDidMount of Class1, write:
componentDidMount() {
AsyncStorage.getItem('value').then((val) => {
this.setState({ value: val });
})
}
Then in your Class1 add a method which will generate the class based on state value:
createClass() {
// do something with the value
if (this.state.value === somevalue) {
return (
<Class2 />
)
}
return null;
}
And in your render method of Class1, type:
render() {
return (
{ this.createClass() }
)
}
You can set it to state, for example:
componentDidMount() {
AsyncStorage.getItem('someValue', (e, someValue) => {
this.setState({someValue})
}
}
Then you can use someValue from state in your render.
Currently, in addition to the async render issue, since you're already passing in a callback to AsyncStorage.getItem(), I'm not sure what using async/await would do.

Close a dropdown when an element within it is clicked

I'm working on a Notification feature in my app (pretty much like Facebook notifications).
When I click a button in the header navigation, the dropdown opens and shows the notification list. The notification has a Link (from react-router) in it.
What I need to do is to close the dropdown whenever a Link is clicked.
Here's roughly the hierarchy I currently have:
Header > Navigation > Button > Dropdown > List > Notification > Link
Since the dropdown functionality is used more that once, I've abstracted its behavior away into a HOC that uses render prop:
export default function withDropDown(ClickableElement) {
return class ClickableDropdown extends PureComponent {
static propTypes = {
children: PropTypes.func.isRequired,
showOnInit: PropTypes.bool,
};
static defaultProps = {
showOnInit: false,
};
state = {
show: !!this.props.showOnInit,
};
domRef = createRef();
componentDidMount() {
document.addEventListener('mousedown', this.handleGlobalClick);
}
toggle = show => {
this.setState({ show });
};
handleClick = () => this.toggle(true);
handleGlobalClick = event => {
if (this.domRef.current && !this.domRef.current.contains(event.target)) {
this.toggle(false);
}
};
render() {
const { children, ...props } = this.props;
return (
<Fragment>
<ClickableElement {...props} onClick={this.handleClick} />
{this.state.show && children(this.domRef)}
</Fragment>
);
}
};
}
The HOC above encloses the Button component, so I have:
const ButtonWithDropdown = withDropdown(Button);
class NotificationsHeaderDropdown extends PureComponent {
static propTypes = {
data: PropTypes.arrayOf(notification),
load: PropTypes.func,
};
static defaultProps = {
data: [],
load: () => {},
};
componentDidMount() {
this.props.load();
}
renderDropdown = ref => (
<Dropdown ref={ref}>
{data.length > 0 && <List items={this.props.data} />}
{data.length === 0 && <EmptyList />}
</Dropdown>
);
render() {
return (
<ButtonWithDropdown count={this.props.data.length}>
{this.renderDropdown}
</ButtonWithDropdown>
);
}
}
List and Notification are both dumb functional components, so I'm not posting their code here. Dropdown is pretty much the same, with the difference it uses ref forwarding.
What I really need is to call that .toggle() method from ClickableDropdown created by the HOC to be called whenever I click on a Link on the list.
Is there any way of doing this without passing that .toggle() method down the Button > Dropdown > List > Notification > Link subtree?
I'm using redux, but I'm not sure this is the kind of thing I'd put on the store.
Or should I handle this imperatively using the DOM API, by changing the implementation of handleGlobalClick from ClickableDropdown?
Edit:
I'm trying with the imperative approach, so I've changed the handleGlobalClick method:
const DISMISS_KEY = 'dropdown';
function contains(current, element) {
if (!current) {
return false;
}
return current.contains(element);
}
function isDismisser(dismissKey, current, element) {
if (!element || !contains(current, element)) {
return false;
}
const shouldDismiss = element.dataset.dismiss === dismissKey;
return shouldDismiss || isDismisser(dismissKey, current, element.parentNode);
}
// Then...
handleGlobalClick = event => {
const containsEventTarget = contains(this.domRef.current, event.target);
const shouldDismiss = isDismisser(
DISMISS_KEY,
this.domRef.current,
event.target
);
if (!containsEventTarget || shouldDismiss) {
this.toggle(false);
}
return true;
};
Then I changed the Link to include a data-dismiss property:
<Link
to={url}
data-dismiss="dropdown"
>
...
</Link>
Now the dropdown is closed, but I'm not redirected to the provided url anymore.
I tried to defer the execution of this.toggle(false) using requestAnimationFrame and setTimeout, but it didn't work either.
Solution:
Based on the answer by #streletss bellow, I came up with the following solution:
In order to be as generic as possible, I created a shouldHideOnUpdate prop in the ClickableDropdown dropdown component, whose Hindley-Milner-ish signature is:
shouldHideOnUpdate :: Props curr, Props prev => (curr, prev) -> Boolean
Here's the componentDidUpdate implementation:
componentDidUpdate(prevProps) {
if (this.props.shouldHideOnUpdate(this.props, prevProps)) {
this.toggle(false);
}
}
This way, I didn't need to use the withRouter HOC directly in my withDropdown HOC.
So, I lifted the responsibility of defining the condition for hiding the dropdown to the caller, which is my case is the Navigation component, where I did something like this:
const container = compose(withRouter, withDropdown);
const ButtonWithDropdown = container(Button);
function routeStateHasChanged(currentProps, prevProps) {
return currentProps.location.state !== prevProps.location.state;
}
// ... then
render() {
<ButtonWithDropdown shouldHideOnUpdate={routeStateHasChanged}>
{this.renderDropdown}
</ButtonWithDropdown>
}
It seems you could simply make use of withRouter HOC and check if this.props.location.pathname has changed when componentDidUpdate:
export default function withDropDown(ClickableElement) {
class ClickableDropdown extends Component {
// ...
componentDidUpdate(prevProps) {
if (this.props.location.pathname !== prevProps.location.pathname) {
this.toggle(false);
}
}
// ...
};
return withRouter(ClickableDropdown)
}
Is there any way of doing this without passing that .toggle() method down the Button > Dropdown > List > Notification > Link subtree?
In the question, you mention that you are using redux.So I assume that you store showOnInit in redux.We don't usually store a function in redux.In toggle function,I think you should dispatch an CHANGE_SHOW action to change the showOnInit in redux, then pass the show data not the function to the children component.Then after reducer dispatch,the react will change “show” automatically.
switch (action.type) {
case CHANGE_SHOW:
return Object.assign({}, state, {
showOnInit: action.text
})
...
default:
return state
}
Link element and data pass
Use the property in Link-to,not data-...Like this:
<Link
to={{
pathname: url,
state:{dismiss:"dropdown"}
}}
/>
And the state property will be found in this.props.location.
give context a little try(not recommend)
It may lead your project to instable and some other problems.(https://reactjs.org/docs/context.html#classcontexttype)
First,define context
const MyContext = React.createContext(defaultValue);
Second,define pass value
<MyContext.Provider value={this.toggle}>
Then,get the value in the nested component
<div value={this.context} />

Refactoring UNSAFE_componentWillReceiveProps

I have an IFrameComponent component, inspired by this post.
It looks basically like this :
class IFrameComponent extends React.Component {
shouldComponentUpdate() {
return false;
}
componentWillReceiveProps(nextProps) {
if(this.props.content !== nextProps.content) {
const html = getHTMLFromContent();
const fdoc = this.iFrame.contentDocument;
fdoc.write(html);
}
}
render() {
return (<iframe sandbox="..." ref={f => this.iFrame = f} />);
}
}
Now that componentWillReceiveProps is considered unsafe, I'm trying to get rid of it.
The ways React advices to refactor componentWillReceiveProps are basically either to use static getDerivedStateFromProps or componentDidUpdate
Sadly, componentDidUpdate will never get called, because shouldComponentUpdate returns false (and I think this is fine ?) and I wouldn't be able to access this.iFrame reference in the static method getDerivedStateFromProps.
How would one refactor this code ?
I think, One possible way is:
let iFrameRefs = {}
class IFrameComponent extends React.Component {
static getDerivedStateFromProps (props) {
if (iFrameRefs[props.id]) {
const html = getHTMLFromContent();
const fdoc = iFrameRefs[props.id].contentDocument;
fdoc.write(html);
}
return null
}
shouldComponentUpdate() {
return false;
}
render() {
return (<iframe sandbox="..." ref={f => iFrameRefs[this.props.id] = f} />);
}
}
Now from Parent Component pass unique id to each component. You can also manage id in IFrameComponent.
<IFrameComponent id='1' />
<IFrameComponent id='2' />

Categories

Resources