Ensure parent only displays when all childs are loaded - javascript

I am very new to react and I'm struggling with the following. I created a form in react and that form contains a dropdown. I need to reuse that dropdown in multiple pages so I thought to make it a component that is fully responsible to get all data.
In the form component I get all data and one of those fields is the selectedServerDataId. The selectId field contains the ID of the value that needs to be selected in the dropdown.
<snip includes/>
class Arrival extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
formData: {}
};
}
async componentDidMount() {
await fetch(urls.addEditUrl)
.then(response => response.json())
.then(data => {
this.setState({ formData: data });
});
}
<snip some setstate helpers/>
render() {
const { formData } = this.state;
return (
<Form >
<Selector label="select"
onChange={this.UpdateFormSelectData.bind(this, 'selectedServerDataId')}
value={formData.selectedServerDataId}/>
<DateItem label="date"
allowClear={true}
value={formData.date}
onChange={this.updateFormDateData.bind(this, 'date')}/>
<snip more fields.../>
</Form>);
}
}
export default Arrival;
The fetch in the parent component retrieves the data for the edit form including the selectedServerDataId. My child component looks like below:
<snip usings />
class ServerDataSelector extends React.Component {
constructor(props) {
super(props);
this.state = {
options: [],
};
}
async componentDidMount() {
await fetch(URLS.GetServerData)
.then(response => response.json())
.then(data => {
this.setState({ options: data });
});
}
render() {
const { options } = this.state;
return (
<FormItem {...formItemLayout} label={t(this.props.label)} >
<Select setValue={this.props.onChange} getValue={() => this.props.value} selectedOption={this.props.value} name={this.props.label}>
{options.map(d => <Option key={d.id} value={d.id}>{d.value}</Option>)}
</Select>
//TODO add model window to manipulate list.
</FormItem>);
}
}
export default ServerDataSelector
ServerDataSelector { Selector }
Currently when the page renders I first see the ID of the selected value in the dropdown and a split second later I see the actual selected value label.
Is there a way to ensure that the parent component only renders when the childs are completely done loading?

The main issue you have here is that you have two components that have a strict rendering hierarchical relationship (parent -> child) but equal data fetching responsibility.
This leads to the situation where you have to react to data retrieval between multiple components in a specific order and is a non-trivial concurrency problem. One solution would be to use a shared state container (using Redux or the Context API) and have both components save their results into this shared state bucket and then whenever the bucket has all the data, you present it (until then you can show a spinner for example).
A simpler solution would be to just move the fetching into one of the components, eliminate this split data-fetching responsibility altogether, and allow the component fetching the data to also control the rendering.
One way is to get all the data in the parent and have the child just be a pure component that is rendered when the data is received:
class Arrival extends React.Component {
// ...
async componentDidMount() {
const [formData, options] = await Promise.all([
fetch(urls.addEditUrl).then(response => response.json()),
fetch(URLS.GetServerData).then(response => response.json())
]);
this.setState({ formData, options });
}
render() {
const { formData, options } = this.state;
return (
<Form>
{/* ... */}
{formData &&
options && (
<Selector
label="select"
options={this.state.options} {/* pass the options here */}
onChange={this.UpdateFormSelectData.bind(
this,
"selectedServerDataId"
)}
value={formData.selectedServerDataId}
/>
)}
</Form>
);
}
}
Selector is now only a presentational component, no need for state:
const ServerDataSelector = props => (
<FormItem {...formItemLayout} label={t(this.props.label)}>
<Select
setValue={this.props.onChange}
getValue={() => this.props.value}
selectedOption={this.props.value}
name={this.props.label}
>
{this.props.options.map(d => ( {/* use props.options */}
<Option key={d.id} value={d.id}>
{d.value}
</Option>
))}
</Select>
</FormItem>
);
You could optionally show a spinner or a loading indicator:
...
{
formData && options ? (
<Selector
label="select"
onChange={this.UpdateFormSelectData.bind(this, "selectedServerDataId")}
options={this.state.options}
value={formData.selectedServerDataId}
/>
) : (
<Spinner />
);
}
Alternatively, you can have the child fetch all the data and the parent would just render the child.
class ServerDataSelector extends React.Component {
// ...
async componentDidMount() {
const [formData, options] = await Promise.all([
fetch(urls.addEditUrl).then(response => response.json()),
fetch(URLS.GetServerData).then(response => response.json())
]);
this.setState({
value: formData.selectedServerDataId,
options
});
}
render() {
return (
<FormItem {...formItemLayout} label={t(this.props.label)}>
<Select
setValue={this.props.onChange}
getValue={() => this.state.value} {/* use the value from the state */}
selectedOption={this.state.value} {/* use the value from the state */}
name={this.props.label}
>
{this.state.options.map(d => (
<Option key={d.id} value={d.id}>
{d.value}
</Option>
))}
</Select>
</FormItem>
);
}
}
You could also add a spinner within the child to prevent page jank and allow for a nicer UX:
render() {
if (!this.state.value || !this.state.options) {
return <Spinner />;
}
return (
<FormItem {...formItemLayout} label={t(this.props.label)}>
<Select
setValue={this.props.onChange}
getValue={() => this.state.value}
selectedOption={this.state.value}
name={this.props.label}
>
{this.state.options.map(d => (
<Option key={d.id} value={d.id}>
{d.value}
</Option>
))}
</Select>
</FormItem>
);
}

After many tries and errors I found a way to fix this.
In my parent state I added the following object:
this.state = {
formData: null,
loadedComponents: {
parentComponentIsLoaded: false,
childComponent1IsLoaded: false,
childComponent2IsLoaded: false,
}
};
Then I added the following 2 methods in my parent
componentLoaded = (field, data) => {
const { loadedComponents } = this.state;
var fieldName = field + "IsLoaded";
this.setState({ loadedComponents: { ...loadedComponents, [fieldName]: true } });
}
allComponentsLoaded() {
const { loadedComponents } = this.state;
console.log(loadedComponents);
for (var o in loadedComponents) {
if (!loadedComponents[o]) return false;
}
return true;
}
Changed the render method to this:
return (
formData ?
<Form className={this.allComponentsLoaded() ? '' : 'hidden'} >
<ChildComponet1 {someprops} isLoaded={this.componentLoaded}/>
<ChildComponent2 {someprops} isLoaded={this.componentLoaded}/>
<snip />
</Form> : <div />);
And added the following line in the childs componentDidMount method after the fetching of the data.
this.props.isLoaded('childComponet1', true)
Maybe this is very bad react design and should you actually use MEM035's answer, but it does work

Related

React how to call componentDidMount every time when switching between ListItem

When initializing widget component which loads various charts it works as it should, but when switching to another ListItem , componentDidMount do not load when switch to another item. I need to load it, because it fetch required data for it. But the thing is when I am switching between ListItem did not initialize componentDidMount
DashboardSidebar.jsx
class DashboardSidebar extends Component {
constructor(props) {
super(props);
this.state = {
tabLocation: this.props.tabLocation
};
this.onChange = this.onChange.bind(this);
}
onChange(event) {
this.setState({
tabLocation: event.target.value
});
}
render() {
const { classes } = this.props;
const { path } = this.props.match;
const { reports = [], sites = [] } = this.props;
let fromFilterString = "-"
let toFilterString = "-"
return (
<Drawer
className={classes.drawer}
variant="permanent"
classes={{
paper: classes.drawerPaper,
}}>
<div>
<List>
{reports.map((report) => (
<ListItem
onClick={this.onChange} button key={report.id}>
<ListItemIcon>
<DashboardIcon />
</ListItemIcon>
<ListItemText
disableTypography
primary={
<Typography type="body2">
<Link to={`${path}/${report.slug}`} style={{ color: "#000" }}>
{report.name}
</Link>
</Typography>
}
/>
</ListItem>
))}
</List>
</div>
<Divider light />
</Drawer>
)
}
}
This component seems run correct, but when clicking on other ListItem run componentDidUpdate which do not fetch required data for charts. Also I find out that when I changed in MainDashboard component key={i} to key={l.id} is started to hit componentDidMount, but then widget's do not load, but from Console I can see that it hit componentDidMount and fetch data which I console.log() .
MainDashboard.jsx
class MainDashboard extends Component {
componentDidUpdate(prevProps) {
if (this.props.match.params.report === prevProps.match.params.report) {
return true;
}
let widgets = {};
let data = {};
let layout = {};
fetch(...)
.then(response => response.json())
.then(data => {
...
this.setState({dashboard: data, isLoading: false, layouts: layout });
})
}
componentDidMount() {
this.setState({ mounted: true, isLoading: true });
let widgets = {};
let data = {};
let layout = {};
fetch(...
})
.then(response => response.json())
.then(data => {
...
this.setState({dashboard: data, isLoading: false, layouts: layout });
})
}
sortWidgets(widgets) {
...
return widgets;
}
generateDOM() {
return _.map(this.state.dashboard.widgets, function(l, i) {
....
return (
<div key={i}>
<ChartWidget
visualization={l.visualization}
name={l.visualization.name}
</div>
);
}.bind(this));
}
render() {
return (
...
);
}
}
You have three option:
1- Use another life cycle such as componentDidUpdate, fetch the new data if there is particular change in props or state
2- Use a new Key, if you use a new key on a component, it will trigger a componentDidMount, because the component is rendered again as a new entity
3- Use react.ref
I think you should read up on all three choices and choose one that will fit you the best.
So componentDidMount is actually being hit but nothing is happening in terms of data updates? in that case, your component loads/mounts first and whatever needs to happen doesn't trigger a re-render, I would look into using another lifecycle method to ensure that your data is updated.
I'm not sure if you're working on a legacy system but if upgrading to functional components is an option, I would recommend using the lifecycle method useEffect because it replaces many lifecycle methods like componentDidMount , componentDidUpdate and the unsafe componentWillUnmount and will make your life a whole lot easier and more efficient.
componentDidMount is only executed when a component is mounted.
A state update in DashboardSidebar would not cause BaseDashboard to be re-mounted so componentDidMount will not be re-executed for BaseDashboard.
Have you tried fetching the data in the onChange event handler (for switching to another ListItem) instead?

How to assign Items to picker getting a list of data as a response in the form of an array?

I am facing a problem with adding the data to picker.item for which the data is in the form of a single array.
I have tried using the map and push for the array, but nothing seems to be working.
My array is as follows
[ "ABC", "XYZ", "123", "aaa"]
class XYZ extends Component {
constructor(props) {
super(props);
this.state = { statelist: '' };
list() {
fetch('myURL', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
.then((response) => {
return response.json();
})
.then((data) => {
data.map((item,key)=>{
console.log(item,key,'item and key')
return(
<Item label={item} value={item} />)
})
}
async onStateChange(value) {
await this.setState({
statelist: value
});}
render() {
return (
<View >
<Picker
selectedValue={this.state.abc}
onValueChange={this.onStateChange.bind(this)}>
this.list()}
</Picker>
</View>
);
};
}
export default withNavigation(XYZ);
I want a dropdown consisting of 5 options as shown above which should be all the 5 elements of the array.
You are not returning the data from your call, and you are also handling an async call. You should get the data on each render, or once in componentDidMount(), and set the state.
const DATA = [1, 2, 3, 4, 5, 6]
class Select extends React.Component {
constructor() {
super()
this.state = {}
}
getList() {
return Promise.resolve(DATA).then(data => data.map(item => <option label={item} value={item} />))
}
componentDidMount() {
this.getList().then(data => this.setState({ list: data }))
}
render() {
return (
<select onChange={this.onStateChange}>
{this.state.list || null}
</select>
)
}
}
ReactDOM.render(
<Select />,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
In order to trigger an update on the component to add items, in your case you should use React state. In fact, using fetch you are doing an asynchronous operation that does not return immediately the result.
In your then()callback in the fetch, you save data into a state variable if you use hooks in function components ( look this link: https://reactjs.org/docs/hooks-reference.html#usestate ), or using this.setState({data: data}) if you use class components.
Then in the return statement, you use the data variable (for function components):
return <Picker>
{ data.map((item)=> <Item label={item} value={item} />) }
<Picker>
or in render method (for class components):
<Picker>
{ this.state.data.map((item)=> <Item label={item} value={item} />) }
<Picker>
In any case, I can help you better if you share your file, otherwise I cannot write a complete example.
here you can find a sample of what I meant using state of component.
https://codesandbox.io/s/sample-select-from-array-190823-u1oux

How to show tooltips for react-select?

I need to show tooltips for react-select container (not for a separate options) using react-tooltip library.
I made my own SelectContainer component based on the original one and added there data-tip and data-for HTML attributes. Tooltip shows up but when I change selected value it disappears and is not displayed any more.
Here is my code:
const getSelectContainer = options => props => {
return (
<components.SelectContainer
{...props}
innerProps={{
...props.innerProps, ...{
'data-tip': options.tooltipText,
'data-for': options.tooltipId,
}
}}
/>
)
}
const CustomSelect = (props) => {
const tooltipId='tooltip_id'
const tooltipText='tooltip'
const selectedOption = colourOptions.filter(option => option.value === props.value)
return (
<div>
<ReactTooltip effect="solid" html={true} place="bottom" id={tooltipId} />
<Select
defaultValue={colourOptions[4]}
value={selectedOption}
options={colourOptions}
classNamePrefix="react-select"
onChange={item => props.onChange(item.value)}
className="my-select"
components={{
SelectContainer: getSelectContainer({
tooltipText:tooltipText,
tooltipId:tooltipId
})
}}
/>
</div>
)
}
class Page extends Component {
constructor (props) {
super(props)
this.state = {
selectValue: 'red'
}
}
render () {
const onChange = (value)=> {
this.setState({selectValue: value})
//alert(value)
}
return (
<CustomSelect
value={this.state.selectValue}
onChange={onChange}>
</CustomSelect>
)
}
}
See full example here:
If I wrap Select with another <div> and assign tooltip HTML attributes to it everything works correctly but I don't want to add one more DOM element just for that.
What can I do to show tooltips after changing selection?
Try rebuilding the react-tooltip when the state changes.
useEffect(() => {
ReactTooltip.rebuild();
}, [state]);

Skills list is not updated when new skill is added

There is an accordion of Skills and Experiences in background component. When the title on the skills accordion is clicked then the modal will pop up with skill form which when filled and submitted then the modal closes and the list of skill should get update but it is not updating. I have to refresh to see the changes in the skill list. Here is how i have done
class Background extends React.PureComponent {
state = {
show: false,
componentName: null,
activeIndex: 0
};
handleModal = (action, componentName) =>
this.setState(state => ({
show: action,
componentName
}));
render() {
const { show, activeIndex, componentName } = this.state;
return (
<React.Fragment>
<ProfileWrapper>
<Query query={BACKGROUND_QUERY}>
{({
data: { experiences, skills, languages, educations } = {},
loading
}) => {
if (loading) {
return <h1>Loading...</h1>;
} else {
return (
<Grid columns={2}>
<Accordion
index={1}
onToggle={this.handleToggle}
css="max-width: 100%; min-width: 200px;"
>
<Accordion.Title>
<Title>
Skills (
{`${skills !== undefined && skills.edges.length}`})
</Title>
</Accordion.Title>
<Accordion.Content>
<Content>
{skills !== undefined && skills.edges.length > 0 && (
<span>
{skills.edges.map(skill => {
return (
<React.Fragment key={skill["node"].id}>
<span key={skill["node"].id}>
<Chip>{skill["node"].title}</Chip>
</span>
</React.Fragment>
);
})}
</span>
)}
</Content>
</Accordion.Content>
</Accordion>
<Modal
position="centerCenter"
open={show}
onClose={() => this.handleModal(false, null)}
>
<React.Fragment>
{componentName !== null &&
componentName === "experiences" && (
<Experiences experiences={experiences} />
)}
{componentName !== null &&
componentName === "skills" && (
<Skills skills={skills} />
)}
</React.Fragment>
</Modal>
</Grid>
);
}
}}
</Query>
</ProfileWrapper>
</React.Fragment>
);
}
}
export default Background;
const Skills = ({ handleSubmit, ...props }) => {
const formSubmit = async (val, mutation) => {
const {
data: { skill: response }
} = await mutation({
variables: val
});
console.log('response', response);
if (response.success) {
props.closeModal();
toast.success("New Skill Added!");
}
};
return (
<React.Fragment>
<Mutation mutation={CREATE_SKILL}>
{mutation => {
return (
<form onSubmit={handleSubmit(val => formSubmit(val, mutation))}>
<Field name="title" label="Title" component={TextField} />
<Button>
<Button.Primary>Add Skill</Button.Primary>
<Button.Secondary>Cancel</Button.Secondary>
</Button>
</form>
);
}}
</Mutation>
</React.Fragment>
);
};
export default compose(
reduxForm({
form: "skillsProfile",
enableReinitialize: true,
destroyOnUnmount: false
})
)(Skills);
why optimistic ui update is not working here when Query is done in background component?
From the docs:
Sometimes when you perform a mutation, your GraphQL server and your Apollo cache become out of sync. This happens when the update you’re performing depends on data that is already in the cache; for example, deleting and adding items to a list. We need a way to tell Apollo Client to update the query for the list of items. This is where the update function comes in!
There's no way for Apollo to know what sort of operations your server is doing when it executes a mutation (adding one or more rows, deleting a row, etc.) -- all it has to go off is the data that's returned by the mutation. It can match this data against objects that are already in the cache and update them accordingly, but that's it. If there are any fields that were cached and need to be updated as a result of your mutation, you need to explicitly tell Apollo how to do this (side note, these could be fields that return a List of Skills, but they could be literally any other fields that were impacted by the mutation).
Your update function for adding a skill, for example, would look something like this:
update={(cache, { data: { addSkill } }) => {
// assumes addSkill resolves to a single Skill
const { skills, ...rest } = cache.readQuery({ query: BACKGROUND_QUERY });
cache.writeQuery({
query: BACKGROUND_QUERY,
data: { skills: skills.concat([addSkill]), ...rest },
});
}}
See the docs for additional examples. It's also worthwhile noting that when using readQuery or writeQuery, you will need to pass in the appropriate variables if your query takes any.
While you can refetch your queries (for example, by specifying them as part of refetchQueries), it's largely unnecessary for simple updates to the cache and obviously slower than just using update since it requires another round-trip to your server. Additionally, update even works with optimistic updates to your UI.

ReactJS instant Search with input

Im making my first react project. Im new in JS, HTML, CSS and even web app programming.
What i want to do it is a Search input label. Now its look like this:
Like you can see i have some list of objects and text input.
I Have two components, my ProjectList.js with Search.js component...
class ProjectsList extends Component {
render() {
return (
<div>
<Search projects={this.props.projects} />
<ListGroup>
{this.props.projects.map(project => {
return <Project project={project} key={project.id} />;
})}
</ListGroup>
</div>
);
}
}
export default ProjectsList;
... and ProjectList.js displays Project.js:
How looks Search.js (its not ended component)
class Search extends Component {
state = {
query: ""
};
handleInputChange = () => {
this.setState({
query: this.search.value
});
};
render() {
return (
<form>
<input
ref={input => (this.search = input)}
onChange={this.handleInputChange}
/>
<p />
</form>
);
}
}
export default Search;
My project have name property. Could you tell me how to code Search.js component poperly, to change displaying projects dynamically based on input in text label? for example, return Project only, if text from input match (i want to search it dynamically, when i start typing m... it shows all projects started on m etc).
How to make that Search input properly? How to make it to be universal, for example to Search in another list of objects? And how to get input from Search back to Parent component?
For now, in react dev tools whatever i type there i get length: 0
Thanks for any advices!
EDIT:
If needed, my Project.js component:
class Project extends Component {
state = {
showDetails: false
};
constructor(props) {
super(props);
this.state = {
showDetails: false
};
}
toggleShowProjects = () => {
this.setState(prevState => ({
showDetails: !prevState.showDetails
}));
};
render() {
return (
<ButtonToolbar>
<ListGroupItem className="spread">
{this.props.project.name}
</ListGroupItem>
<Button onClick={this.toggleShowProjects} bsStyle="primary">
Details
</Button>
{this.state.showDetails && (
<ProjectDetails project={this.props.project} />
)}
</ButtonToolbar>
);
}
}
export default Project;
To create a "generic" search box, perhaps you could do something like the following:
class Search extends React.Component {
componentDidMount() {
const { projects, filterProject, onUpdateProjects } = this.props;
onUpdateProjects(projects);
}
handleInputChange = (event) => {
const query = event.currentTarget.value;
const { projects, filterProject, onUpdateProjects } = this.props;
const filteredProjects = projects.filter(project => !query || filterProject(query, project));
onUpdateProjects(filteredProjects);
};
render() {
return (
<form>
<input onChange={this.handleInputChange} />
</form>
);
}
}
This revised version of Search takes some additional props which allows it to be reused as required. In addition to the projects prop, you also pass filterProject and onUpdateProjects callbacks which are provided by calling code. The filterProject callback allows you to provide custom filtering logic for each <Search/> component rendered. The onUpdateProjects callback basically returns the "filtered list" of projects, suitable for rendering in the parent component (ie <ProjectList/>).
The only other significant change here is the addition of visibleProjects to the state of <ProjectList/> which tracks the visible (ie filtered) projects from the original list of projects passed to <ProjectList/>:
class Project extends React.Component {
render() {
return (
<div>{ this.props.project }</div>
);
}
}
class ProjectsList extends React.Component {
componentWillMount() {
this.setState({ visibleProjects : [] })
}
render() {
return (
<div>
<Search projects={this.props.projects} filterProject={ (query,project) => (project == query) } onUpdateProjects={ projects => this.setState({ visibleProjects : projects }) } />
<div>
{this.state.visibleProjects.map(project => {
return <Project project={project} key={project.id} />;
})}
</div>
</div>
);
}
}
class Search extends React.Component {
componentDidMount() {
const { projects, filterProject, onUpdateProjects } = this.props;
onUpdateProjects(projects);
}
handleInputChange = (event) => {
const query = event.currentTarget.value;
const { projects, filterProject, onUpdateProjects } = this.props;
const filteredProjects = projects.filter(project => !query || filterProject(query, project));
onUpdateProjects(filteredProjects);
};
render() {
return (
<form>
<input onChange={this.handleInputChange} />
</form>
);
}
}
ReactDOM.render(
<ProjectsList projects={[0,1,2,3]} />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.0.0/umd/react-dom.production.min.js"></script>
<div id="react"></div>
I will assumes both your Search and ProjectList component have a common parent that contains the list of your projects.
If so, you should pass a function into your Search component props, your Search component will then call this function when the user typed something in the search bar. This will help your parent element decide what your ProjectsLists needs to render :
handleInputChange = () => {
this.props.userSearchInput(this.search.value);
this.setState({
query: this.search.value
});
};
And now, here is what the parent element needs to include :
searchChanged = searchString => {
const filteredProjects = this.state.projects.filter(project => project.name.includes(searchString))
this.setState({ filteredProjects })
}
With this function, you will filter out the projects that includes the string the user typed in their names, you will then only need to put this array in your state and pass it to your ProjectsList component props
You can find the documentation of the String includes function here
You can now add this function to the props of your Search component when creating it :
<Search userSearchInput={searchChanged}/>
And pass the filtered array into your ProjectsList props :
<ProjectsList projects={this.state.filteredProjects}/>
Side note : Try to avoid using refs, the onCHnage function will send an "event" object to your function, containing everything about what the user typed :
handleInputChange = event => {
const { value } = event.target
this.props.userSearchInput(value);
this.setState({
query: value
});
};
You can now remove the ref from your code

Categories

Resources