An object component - javascript

I'm learning React and I'm trying to render a form with a few different form-steps (details, payments, confirm...) from a UI library that has Form component ready, which iterates through those form-steps.
The issue is that each 'step' component looks more like an actual JS object:
export const DetailStep = (
<div>Details screen with inputs, texts and images...</div>
);
It doesn't have the = () => so it's not an actual functional component, so I can't add hooks or functions into it.
That's the array of steps:
const stepPages = [
DetailStep,
PaymentStep,
ConfirmStep
];
And that's the actual Form component that iterates through the array of steps:
<Form
onSubmitClick={onStepSubmit}
render={formRenderProps => {stepPages[step]} }
/>
If I'll change any step to a functional component (DetailStep = () => ...), the step will be blank and won't be rendered.
How can I fix that / just add hooks to each 'step' component?

JSX declarations are converted into React.createElement function calls, this does mean that the stepPages in your example are objects (just that they are react objects)
The magic that turns them into function components happens in the render prop of the form:
<Form
onSubmitClick={onStepSubmit}
render={formRenderProps => {stepPages[step]} }
/>
You can see that it's passing a function in here, that returns the jsx objects. This is kinda the same as putting the jsx directly in the render function.
Although I notice that in your example, the jsx isn't being returned, so I would expect this example to not work correctly.
The longform equivalent would be something like this:
<Form
onSubmitClick={onStepSubmit}
render={formRenderProps => {
if (step === 0) {
return (
<div>Details screen with inputs, texts and images...</div>
)
} else if (step === 1) {
return (
<div>Details screen with inputs, texts and images...</div>
)
} // etc
}}
/>
EDIT to answer question in comments
You're right, so how can I add hooks into those object components?
If each of the form steps needs to use hooks and manage it's internal state, then I would make them functional components instead of JSX objects, then pick the right component from the stepPages map and render it like so:
export const DetailStep = (props) => {
// hooks and stuff here
return (
<div>Details screen with inputs, texts and images...</div>
);
}
const StepComponent = stepPages[step];
return (
<Form
onSubmitClick={onStepSubmit}
render={formRenderProps => <StepComponent {...formRenderProps} />}
/>
);

Related

How to call functions set as properties of child functional components in React?

I have an app with multiple filters that share the same logic but with different inputs. Every filter has a different combination of inputs and some of those inputs are reused in different places, so I created some reusable components to be used like this:
<Filter>
<Filter.Input1 />
<Filter.Input2 />
</Filter>
Filter component manipulates the query string and every Input component uses different letters and logic to set it. For now, the logic related to query string is inside Filter:
const Filter = ({children}) => {
const searchParams = getParams(); // returns an object with the query string params
// Simplified version of current code to populate defaultValues
const defaultValues = {
a: searchParams?.a, // query string param for Input1
b: searchParams?.b, // query string param for Input2
}
// Here is a hook that receives the defaultValues
// On a second render this hook will ignore any new defaultValues
const onSubmit = (values) => {
// More logic related to query string params
}
return (
<form onSubmit={onSubmit}>
{React.children.map(...) // Set some props on Input elements
</form>
)
}
The problem here is that I have to include the logic to populate the defaultValues for every possible Input component, no matter if it's being used and now it's growing in complexity. I want to split that logic into the child components and loop through them to invoke functions with that logic, so now I'm trying to do something like this:
const Input1 = () => {...}
Input1.parseParams = (stringParams) => ({ a: stringParams.?a }) // Some components have a more complex logic
const Input2 = () => {...}
Input2.parseParams = (stringParams) => ({ b: stringParams.?b })
const Filter = ({children}) => {
const searchParams = getParams();
const defaultValues = React.children.map(children, child => {
if (child?.type && child?.type?.parseParams) {
return child.type.parseParams(searchParams);
}
})
}
It works, but I didn't see anything on the docs or somewhere else about the property type inside child. Also, I was aware of it containing the parseParams function after doing a console.log. I have never seen an approach like this, so my questions are:
Where can I find documentation about type?
Is this a valid approach or there is a better way to do it? I'm afraid that type could be an internal property that is not intended to be used like this.
PD: I think using refs will not work here because I need to do the form initialization on the first render and refs are assigned to the children on the return.
I haven't seen earlier such approach or use of type, but I have a suggestion,
Since you wrote some specific logic for each component (Input1, Input2), why don't you just pass searchParams to them and let them decide what to do with it?
You can make something like this:
React.children.map(children, child => {
if (child?.type && child?.type?.parseParams) {
return React.cloneElement(child, { searchParams });
}
})
UPD: I noticed that you might want just to get some defaultValues in your parent filter component, I think you can provide your child components a method like "setDefaultValue" and pass searchParams to it. After your child component render, it will make all necessary logic and call setDefaultValue which will set state in your parent component. It should work

React.js State updating correctly only once, then failing [duplicate]

This question already has answers here:
Using a Set data structure in React's state
(2 answers)
Closed 1 year ago.
I have a parent and a child component. There are 3 props the parent provides out of which 1 is not updating correctly.
Following is the parent component. The prop in question is selectedFilters (which is an object where keys are mapped to sets) and the relevant update function is filterChanged (this is passed to the child)
import filters from "../../data/filters"; //JSON data
const Block = (props) => {
const [selectedFilters, setSelectedFilters] = useState({versions: new Set(), languages: new Set()});
console.log(selectedFilters);
const filterChanged = useCallback((filter_key, filter_id) => {
setSelectedFilters((sf) => {
const newSFSet = sf[filter_key]; //new Set(sf[filter_key]);
if (newSFSet.has(filter_id)) {
newSFSet.delete(filter_id);
} else {
newSFSet.add(filter_id);
}
const newSF = { ...sf, [filter_key]: new Set(newSFSet) };
return newSF;
});
}, []);
return (
<FilterGroup
filters={filters}
selectedFilters={selectedFilters}
onFilterClick={filterChanged}
></FilterGroup>
);
};
export default Block;
The following is the child component: (Please note that while the Filter component runs the filterChanged function, I think it is irrelevant to the error)
import Filter from "./Filter/Filter";
const FilterGroup = (props) => {
const { filters, selectedFilters, onFilterClick } = props;
console.log(selectedFilters);
const filter_view = (
<Container className={styles.container}>
{Object.keys(filters).map((filter_key) => {
const filter_obj = filters[filter_key];
return (
<Filter
key={filter_obj.id}
filter_key={filter_key}
filter_obj={filter_obj}
selectedFilterSet={selectedFilters[filter_key]}
onFilterClick={onFilterClick}
/>
);
})}
</Container>
);
return filter_view;
};
export default FilterGroup;
When running the application, I find that the selectedFilters updates correctly only once. After that, it only changes temporarily in the main Block.tsx, but eventually goes back to the first updated value. Also, FilterGroup.tsx only receives the first update. After that, it never receives any further updated values.
Here are the logs:
After some experimentation, it is clear that the problem originates from the filterChanged function. But I cannot seem to figure out why the second update is temporary AND does not get passed on to the child.
Any ideas? Thanks in advance.
(If any other info is required, pls do mention it)
I don't think you actually want your filterChanged function to be wrapped with useCallback, especially with an empty deps array. with the empty deps array, I believe useCallback will fire once on initial render, and memoize the result. You may be able to add filter_key and filter_id to the dependency array, but useCallback tends to actually slow simple functions down, instead of adding any real performance benefit, so you may just want to get rid of the useCallback completely and switch filterChanged to a regular arrow function.

How to Pass React form state from app to multiple components (functional)

I'm still fairly new to React, so I'm sorry if this is a repetitive post. I'm working with React to create a form that spreads across different pages. The idea is to have the type of form you'd receive from a job application. One that has multiple steps.
I have different components made for each step of the form. The first page is a home page. Imagine a button to take you to the create page. Then you see the Create page. There is a small form with an amount and name. When you click the next page, you'll see the Customize page. There you can edit your budget by priority.
Here in my poorly drawn picture, I have the app obviously, and a budget homepage to manage state across it's children. I figured I should do it this way because, well it's the only way I know how. The problems I have, are one: I don't know how to properly pass state throughout the components. Two: I don't even know if what I'm doing is correct. The React docs use forms in Class based components, which sort of helps, but I can't configure it to my problem.
Any help or suggestions are greatly appreciated. Also I want to show code from the Budget Page.
import React, { useState, useEffect, useRef } from "react";
import BudgetResultsPage from './BudgetResultsPage';
const [count, setState] = useState(0);
const handleStateCount = () => {
if(count>3) {count = 0;}
return setState(count + 1);
}
if(count === 0) {
console.log(`homepage state ${count}`)
return (
<div className={Styles.Calculator}>
<BudgetHomePage setState={count} handleStateCount={handleStateCount}/>
</div>
)
} else if(count===1) {
console.log(`budget create state ${count}`)
return (
<div className={Styles.Calculator}>
<CalculatorHeader />
<CreateBudget setState={count} handleStateCount={handleStateCount} setAmount={amount} handleAmount={handleAmount}/>
</div>
)}
There are obviously more imports for the other pages and more code passed in the component. This is just a snippet of the way I'm handling the components.
It's probably silly, but I'm creating a count in state, then when part of the form is submitted, the count goes up and it changes the page based on what the count is at. Works great for just one state, but I'm having problems with adding more state to it.
Thanks again!
I have written an example functional components illustrating some of your questions. I wrote this at home and only had notepad++ so might not compile if you copy paste. The comments will explain your questions
import React from 'react';
import Create_budget_page from './Create_budget_page.js';
const BudgetPage = props => {
// State 1: assigning one value to the state.
// State 2: assinging a list to the state.
// State 3: assinging a object to the state.
const [simple_state, set_simple_state] = useState(false);
const [list_state, set_list_state] = useState(["Hello","World","!"]);
const [object_state, set_object_state] = useState(
{
curmenu: "human_bios",
height: "196cm",
weight: "174lbs",
eye_colour: "blue",
hair_colour: "dirty_blonde"
}
);
// there are several possiblities, here's one with a list of objects
const [list_objects, set_list_objects] = useState(
[
{count: 69, wasClicked: false},
{count: 420, wasClicked: true},
// endless possibilities, the tricky part is properly correctly updating your states deep in the list
{integers: [1,5,2,3], keys: {isAlive: false, cur_menu: "config_menu"}}
]
);
// this function updates the state that stores an object
// arguments:
// new_values_object: the programmer passes in a object to the function with the values they want to update
// an example of calling this function in a child functional component:
// props.update_object_state({height: "165cm", weight: "143lbs", hair_colour: "black"});
function update_object_state(new_values_object){
set_object_state(prevState => {
const new_obj = prevState;
// loop through object keys and update the new_obj with the object passed in as a argument
for (var key in new_values_object){
new_obj[key] = new_values_object[key];
}
// returning the new_object will update the object_state
return new_obj;
})
}
// conditionally render based on state
switch(object_state["curmenu"]){
case "home_page":
return (
// pass in
// all props, 1 prop, state
<Create_budget_page {...props} parent_id={prop.parent_id} simple_state={simple_state} list_objects={list_objects}/>
) ;
break;
// pass in function
case "human_bios":
return (
<div className="error_page" onClick={() => {props.update_parent_state({error_occured: true})}}>This is an Error Page, click me to report diagnostics</div>
);
break;
// if none of cases are met default code executes
default:
// renders nothing
return null;
}
}
Which kind of problems are you experiencing? I assume that the part of code you left there is inside a component body (a function, as you are using functional way).
You should have no problems passing multiple props to child components even though you have to keep in mind other approaches (excesive number of props means something is wrong in general...)
Let me know if you have more questions.
EDIT:
Suppose you have that ResultsPage child component and in your parent component you do something like:
<ResultsPage someProp={someValue} someOtherProp={someOtherValue} />
Then in your child component you can manage those props like this:
const ResultsPage = (props) => {
// props is an object like this: { someProp: someValue, someOtherProp: someOtherValue}
... // and in the body of the function you can manipulate it however you want
}
Generally it is better to destructure your props variable in the param directly instead of always do props.someProp, etc. like this:
const ResultsPage = ({someProp, someOtherProp}) => { ... }
More info about destructure here

React parent component check if child will render

I have a child component that depending on some of its props will end up rendering something or not. The render function of the children looks something like this:
render() {
if (props.a == 'foo' && props.b == 'bar') {
return (<p> Hey There </p>);
} else if {props.a == 'z') {
return (<p> Hey There </p>);
} // more conditions
} else {
return null
}
}
In the parent component I am rendering several child components, and I need to know how many of them will render, because depending on that number I will do something or not. I don't want to repeat the conditional logic from the child to the parent, but I don't know how from a parent I can find out if the children will render or not.
What you're trying to do is a bit of an anti-pattern in React.
If I understand your question, the children render output would influence their parent's render ouptput, which is likely to get you stuck in a render loop.
I suggest you keep the children components as simple as possible and hoist the conditional logic to the parent which will allow you to count how many children you'll be rendering in place.
Please let me know if I got your question wrong.
Although I agree with perpetualjourney's answer, I thought I could give you a possibility of counting the children.
The easiest would be to save the rendering of the children first to a variable and then to render that result.
var kids = this.props.items.map( (k, i) => <Kid condition={k} key={i} /> );
var totalKids = kids.reduce( (k, i) => k + (i.type( i.props ) ? 1 : 0), 0);
Now, this will render your children twice, so it is not the best when you already have a performance heavy method
const Kid = ({ condition }) => condition % 3 === 0 ? <h1>{ condition }</h1> : null;
class ConditionalKids extends React.Component {
render() {
var kids = this.props.items.map( (k, i) => <Kid condition={k} key={i} /> );
var totalKids = kids.reduce( (k, i) => k + (i.type( i.props ) ? 1 : 0), 0);
return <div>
<p>Rendered in total { totalKids }</p>
{ kids }
</div>;
}
}
const items = [...new Array(5)].map( i => parseInt( Math.random() * 10 ) );
console.log( items );
const target = document.querySelector('#container');
ReactDOM.render( <ConditionalKids items={items} />, target );
<script id="react" src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.2/react.js"></script>
<script id="react-dom" src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/15.6.2/react-dom.js"></script>
<div id="container"></div>
One way of handling this problem is to use state.
In this CodeSandbox example, I use the if/else logic to update the component's state. This is done inside the componentWillReceiveProps lifecycle method.
Your render method is simplified to a switch statement.
If you want to call an imperative API based on the if/else logic you have, you can do that inside your componentDidUpdate lifecycle method. This is also simplified to a switch statement.
That's a very pertinent question that affects applications of any complexity. Unfortunately there's no clear way to model this using React today. I've done a ton of React and would recommend what #perpetualjourney said: hoist the logic to a common parent. Sometimes you'll need to move it way up, but do it instead of messing around with React.Children.map or React.Context since they'd create dependency between the components and would make it harder for you to move them around if needed.
const hasA = computeHasA({ ... });
const hasB = computeHasB({ ... });
const hasAny = hasA || hasB;
if (hasAny) {
return (
<Tabs>
{hasA && <Tab title="A">Some content</Tab>}
{hasB && <Tab title="B">Another content</Tab>}
</Tabs>
);
}
When you really can't move to logic upwards, for example, if the children do network requests, which is probably an edge-case, I'd recommend passing a prop used to report whether they have content or not and storing it into the parent's state:
const [numberOfRenderedChildren, setNumberOfRenderedChildren] = useState(0);
const incrementNumberOfRenderedChildren = () => {
// Use a callback to prevent race conditions
setNumberOfRenderedChildren(prevValue => prevValue + 1);
};
return (
<MyContainer isVisible={numberOfRenderedChildren > 0}>
{items.map(item => (
<ComplexChild
key={item.id}
// Avoid spreading props like ...item. It breaks semantics
item={item}
reportHasContent={incrementNumberOfRenderedChildren}
/>
)}
</MyContainer>
);
It could get more complex if you need loaders and error messages, but then I'd fire all the requests in the parent. Try to keep it simple, easy to read, even if it means more verbosity. You won't regret when you need to come back to those files a few weeks later.

Rendering Multiple React Components with .map

This may be a complete noob question, but I am having trouble figuring this out. I have a React component, and inside its render method, I want to render some dynamically passed in components (through props).
Here's my return from the render method (ES6, transpiled with Babel)
return (
<div className={openState}>
/* Shortened for brevity*/
{this.props.facets.map(function(f) {
var CompName = facetMap[f.type]
return <CompName key={f.id} />
})}
/* Shortened for brevity*/
</div>
)
facetMap is simply a map to match up a string to the "type" passed in. I can't seem to get the components to actually render on the page.
I had the result of the .map() method stored in a var, and then logged that to the console, and it showed an array of components. However {facetComponents} in the render method had the same results.
EDIT, for those interested in the solution
Because I genuinely could not find an answer anywhere, here's my completed solution, thanks to #Mathletics
//Components imported using ES6 modules
import OCFacetText from "./oc-facet-text"
// Etc...
//Later on, in my render method:
let facetMap = {
"text": OCFacetText,
"number": OCFacetNumber,
"numberrange": OCFacetNumberRange,
"boolean": OCFacetBoolean,
"checkbox": OCFacetCheckbox,
// Etc, etc...
}
let facetComps = facets.map(function(f) {
var CompName = facetMap[f.type]
return <CompName key={f.id} />
})
//Finally, to render the components:
return (
<div className={openState}>
/* Shortened for brevity*/
{facetComps}
/* Shortened for brevity*/
</div>
)
From what I can gather, it sounds like your facetMap is a collection of strings:
var facetMap = {text: 'OCFacetText'}
but it should actually return component classes:
var OCFacetText = React.createClass({...});
var facetMap = {text: OCFacetText};
Then (and only then) you will need to use the createElement method to construct your component dynamically:
var CompName = facetMap[f.type]; // must be a FUNCTION not a string
return React.createElement(CompName, {key: f.id}) />
Annoying one.
If you want dynamic component names you'll have to utilize React.createElement(CompName), where CompName is a string or the class object.
By default react will look for a component named CompName in your above example.
So your render function would look like so:
return (
<div className={openState}>
/* Shortened for brevity*/
{this.props.facets.map(function(f) {
var CompName = facetMap[f.type]
return React.createElement(CompName, {key: f.id}) />
})}
/* Shortened for brevity*/
</div>
)

Categories

Resources