Passing state Array to child components in React - react-native

I'm trying to pass two array variables to a child component in react. I know it's something simple and I'm being stupid but I jest need a hand.
I make two API calls in two components that are in the same file. They should be adding the iterable data into the variable using the set methods. By the time the call to the component comes, the variables are undefined.
What am I doing wrong?
const ShowResults = props => {
const { pk } = props;
const [attributionsData, setAttributionsData] = useState([])
const [interactionsData, setInteractionsData] = useState([])
function ListAttributions({ pk }) {
const { loading, error, data } = useQuery(GET_ATTRIBUTIONS, {
variables: { pk },
});
if ( loading ) return <h2>LOADING... </h2>;
if ( error ) return `Error! ${error}`;
if ( data !== undefined){
setAttributionsData(data.listAttributions.items);
return <div>
{attributionsData.map(({ sk, nominal, organisation, attribution, file_name, datetime_added, exhibit }, index) => (
<AttributionsCard
key={index}
Sk={sk}
Nominal={nominal}
Organisation={organisation}
Attribution={attribution}
FileName={file_name}
FoundInsidePhone={file_name.match(/[0-9]+/g)}
DateTimeAdded={datetime_added}
Exhibit={exhibit}
Pk ={pk}
/>
))}
</div>
}
}
function ListInteractions({ pk }) { //Aug 2022 - This is a working prototype of the query behavious i'm looking for.
const { loading, error, data } = useQuery(GET_INTERACTIONS, {
variables: { pk },
});
if ( loading ) return <h2>LOADING... </h2>;
if ( error ) return `Error! ${error}`;
if ( data !== undefined){
console.log("Interactions Data - " + data );
setInteractionsData(data.listInteractions.items)
return <div>
{interactionsData.map(({ direction, interaction, partner, duration, datetime, exhibit, organisation, file_name, datetime_added }, index) => (
<InteractionsCard
key={index}
Interaction={interaction}
Direction={direction}
Partner={partner}
Duration={duration}
DateTime={datetime}
Exhibit={exhibit}
Organisation={organisation}
FileName={file_name}
DateTimeAdded={datetime_added}
Pk={pk}
/>
))}
</div>
}
}
return (
<div style={{display: "inline-grid", inlineSize: "max-content", width: "100%"}}>
{/* <h2>🚀 Quest has identified the following results against {pk}</h2> */}
<center>
<ListAttributions pk={pk}/>
<ListInteractions pk={pk}/>
</center>
<br/>
<br />
{/* <ShowInteractionsDataGrid pk={pk}/> */}
{attributionsData !== [] &&
// Sep 14th 20:42 - No idea whats going on with this... The ListAttributions components above work fine
<TabbedResults pk={pk} attributionsData={attributionsData} interactionsData={interactionsData} />
}
<br />
<br />
</div>
);
}
ShowResults.propTypes = {
pk: PropTypes.string
};
export default ShowResults

a) You cant setSomeState and then use it immediately. It will not be immediately available within that same function. So the first time you try to map you will have the initialiser of the useState. In this case an empty array.
b) You should do the queries in the parent component, and pass them in as props so there is no need to set parent state
c) Its not good to be setting the state of a parent scope like this. You should move the children to different files (or at least outside of the component) and communicate with props and callbacks. You will save yourself a lot of trouble.
d) This will always be true so TabbedResults will always render:
attributionsData !== []
This is because [] makes a new array. You should use:
attributionsData && attributionsData.length
e) Never set state straight form the render function. Use a useEffect hook to set it based on the change of a property, such as your data suddenly being populated

Related

Export child components and use individually

I have a case where I need to export two child components and use individually.
Much Desired outcome (Extremely simplified):
Controls.js:
const Controller = ( props ) => {
const ControlBoxes = () => {
return(<Button>Move around!</Button>)
}
const MoveableBox = () => {
return(<View>I will be moved! </View>)
}
return {ControlBoxes, MoveableBox}
}
export default Builder
Canvas.js:
import Controller from './controls'
const boxScaleMove = boxes.map((box, index) => {
return (
<Bulilder.MoveableBox key={box.id} box={box}/>
)
}
const boxController = boxes.map((box, index) => {
return (
<Bulilder.ControlBoxes key={box.id} box={box}/>
)
}
return (
...
{boxController}
...
...
{boxScaleMove}
...
)
Any idea how I can achieve this or am I missing something fundamental? The main issue is that I want to avoid resorting to useContext (due to performance reasons in the case of a lot of boxes rendered) and be able to share variables and states between MoveableBox-component and ControlBoxes-component via Controller -parent.
Any help would be greatly appreciated.
You could use the compound component and use a lower level context to avoid re-rendering of the whole tree and share states across your components that way, below I would ilustrate a basic example of how that would work.
const RandomContext = createContext();
export default function Controller({children, ...rest}) {
const [randomState, setRandom] = useState(0);
return (
<RandomContext.Provider value={{ randomState, setRandom }}>
<div {...rest}>{children}</div>
</RandomContext.Provider>
);
}
Controller.ControlBoxes = function (props) {
const { setRnadom } = useContext(RandomContext);
return (
<Button onClick={() => setRandom(2)} {...props}>Move around!</Button>
);
};
Controller.MoveableBox = function (props) {
const { randomState } = useContext(RandomContext);
return randomState ? <View {...props}>I will be moved!</View> : null;
};
And you would use it as:
<Controller>
<Controller.ControlBoxes />
<Controller.MoveableBox />
<Controller>
In the compound components pattern we are leveraging the fact that in javascript when you declare a function you create a function/object combo. Therefor Controller function is both a function and an object, so we can assign properties the the object part of that combo, properties which are in our case ControlBoxes and MoveableBox which are functions themselves.
NOTE you should probably assign named function the the properties of that object, it's easier to debug if the case needed.
Example.Function = function ExampleFunction(props) {
return "Example";
};

Auto Calculate Input Fields

Please could someone help answer this:
I have 2 NumberInput controls, one input and the other is disabled. I need to input number in the first input field, the disabled field to show this number/100. The two NumberInput will have source fields that will save to the current record in the simpleform.
How do I do this in react-admin
Thanks
Easiest way is to use the method described in the docs under section Linking two inputs
In essence: You can create your own input component where you can access the form values via the hook useFormState. Then just assign the desired value transformed the way you want e.g. divided by 100.
Edit
Found one more even cleaner way - using the final-form-calculate to create a decorator and pass it to the <FormWithRedirect /> component like so:
import createDecorator from 'final-form-calculate'
const calculator = createDecorator(
// Calculations:
{
field: 'number1', // when the value of foo changes...
updates: {
number2: (fooValue, allValues) => allValues["number1"] * 2
}
})
...
<FormWithRedirect
...
decorators={[calculator]}
/>
Check out this code sandbox
Using FormDataConsumer
<FormDataConsumer>
{({ formData }) => (
<NumberInput defaultValue={formData.my_first_input / 100} source="second_input"/>
)}
</FormDataConsumer>
Using the useFormState hook
import { useFormState } from 'react-final-form';
...
const { values: { my_first_input }} = useFormState({ subscription: { values: true } });
...
<NumberInput defaultValue={my_first_input / 100} source="second_input"/>
Source: https://marmelab.com/react-admin/Inputs.html#linking-two-inputs
Dynamic
You need to use the useForm hook of react-final-form to make your input dynamic:
import { useForm, useFormState } from 'react-final-form';
...
const {change} = useForm();
const { values: { my_first_input }} = useFormState({ subscription: { values: true } });
useEffect(() => {
change('my_second_input', my_first_input / 100);
}, [change, my_first_input]);
...
<NumberInput defaultValue={my_first_input / 100} source="second_input"/>
I got a shorter solution to this question:
All I did was to do the calculation within FormDataConsumer. Now, I am able to get the calculated value and it updates the correct record in the array.
Thanks
<FormDataConsumer>
{({
formData, // The whole form data
scopedFormData, // The data for this item of the ArrayInput
getSource, // A function to get the valid source inside an ArrayInput
...rest
}) => {
if (typeof scopedFormData !== 'undefined') {
scopedFormData.total = scopedFormData.quantity * scopedFormData.unitprice;
return (
<NumberInput disabled defaultValue={scopedFormData.total} label="Total" source={getSource('total')} />
)
} else {
return(
<NumberInput disabled label="Total" source={getSource('total')} />
)
}
}}

How to get a component from an array and change its props in react native?

I've created some components in a for loop and push them to an array. Can i get any component from this array to change its props -like title ?
fields = [];
for (let i = 0; i < assets.fieldNames[assets.systemLang].length; i++) {
fields.push(
<InfoField
handlePress={() => this.fieldPressed(i)}
key={i}
title={assets.fieldNames[assets.systemLang][i]}
value="" />
);
};
.....
<View style={styles.infoFields}>
{
fields
}
</View>
and i have a function that i need something like this
changeComponentTitle = () => {
fields[indexForComponent].props.title = "new Title"
}
You'll need to make sure that you trigger a re-render in some way after you make the update (perhaps by putting fields in a state variable and calling setState as mentioned in the comments) but you can use cloneElement to essentially update a single prop of an existing element: https://reactjs.org/docs/react-api.html#cloneelement
changeComponentTitle = () => {
fields[indexForComponent] = React.cloneElement(fields[indexForComponent], {title: "new Title"});
}

How to update a single item in FlatList in React Native?

Attention: I have posted an answer down there, personally I think it's the best solution so far. Even though it's not the highest rated answer, but based on the result I'm getting, it is very efficient.
---------------------------------------------Original Question-------------------------------------------------------
Suppose I am writing a Twitter clone, but much simpler. I put each item in FlatList and render them.
To "like" a post, I press the "like" button on the post and the "like" button turns red, I press it again, it turns gray.
This is what I have so far: I store all the loaded posts in this.state, each post has a property called "liked", which is boolean, indicating whether this user has liked this post or not, when user presses "like", I go to state.posts and update the liked property of that post, and then use this.setState to update posts like so:
// 1. FlatList
<FlatList
...
data={this.state.posts}
renderItem={this.renderPost}
...
/>
// 2. renderPost
renderPost({ item, index }) {
return (
<View style={someStyle}>
... // display other properties of the post
// Then display the "like" button
<Icon
name='favorite'
size={25}
color={item.liked ? 'red' : 'gray'}
containerStyle={someStyle}
iconStyle={someStyle}
onPress={() => this.onLikePost({ item, index })}
/>
...
</View>
);
}
// 3. onLikePost
likePost({ item, index }) {
let { posts } = this.state;
let targetPost = posts[index];
// Flip the 'liked' property of the targetPost
targetPost.liked = !targetPost.liked;
// Then update targetPost in 'posts'
posts[index] = targetPost;
// Then reset the 'state.posts' property
this.setState({ posts });
}
This approach works, however, it is too slow. The color of the "like" button flips as I press it, but it usually takes about 1 second before the color changes. What I want is that the color would flip almost at the same time when I press it.
I do know why this would happen, I should probably not use this.setState, because when I do that, the posts state changed, and all posts get re-rendered, but what other approach can I try?
You can set extraData in FlatList:
<FlatList
...
extraData={this.state}
data={this.state.posts}
renderItem={this.renderPost}
...
/>
When state.posts or state.posts's item change, FlatList will re-render.
From FlatList#extradata:
A marker property for telling the list to re-render (since it implements PureComponent). If any of your renderItem, Header, Footer, etc. functions depend on anything outside of the data prop, stick it here and treat it immutably.
Update:
Functional component implementation:
export default function() {
// list of your data
const [list, setList] = React.useState([])
const [extraData, setExtraData] = React.useState(new Date())
// some update on the item of list[idx]
const someAction = (idx)=>{
list[idx].show = 1
setList(list)
setExtraData(new Date())
}
return (
<FlatList
// ...
data={list}
extraData={extraData}
/>
)
}
After updating list, I use setExtraData(new Date()) to tell the FlatList to re-render. Because the new time is different from the previous.
Don't get me wrong, #ShubhnikSingh's answer did help, but I retracted it because I found a better solution to this question, long time ago, and finally I remembered to post it here.
Suppose my post item contains these properties:
{
postId: "-L84e-aHwBedm1FHhcqv",
date: 1525566855,
message: "My Post",
uid: "52YgRFw4jWhYL5ulK11slBv7e583",
liked: false,
likeCount: 0,
commentCount: 0
}
Where liked represents whether the user viewing this post has liked this post, which will determine the color of the "like" button (by default, it's gray, but red if liked == true)
Here are the steps to recreate my solution: make "Post" a Component and render it in a FlatList. You can use React's PureComponent if you don't have any props that you pass to your Post such as an array or object that can be deceptively not shallow equal. If you don't know what that means, just use a regular Component and override shouldComponentUpdate as we do below.
class Post extends Component {
// This determines whether a rendered post should get updated
// Look at the states here, what could be changing as time goes by?
// Only 2 properties: "liked" and "likeCount", if the person seeing
// this post ever presses the "like" button
// This assumes that, unlike Twitter, updates do not come from other
// instances of the application in real time.
shouldComponentUpdate(nextProps, nextState) {
const { liked, likeCount } = nextProps
const { liked: oldLiked, likeCount: oldLikeCount } = this.props
// If "liked" or "likeCount" is different, then update
return liked !== oldLiked || likeCount !== oldLikeCount
}
render() {
return (
<View>
{/* ...render other properties */}
<TouchableOpacity
onPress={() => this.props.onPressLike(this.props.postId)}
>
<Icon name="heart" color={this.props.liked ? 'gray' : 'red'} />
</TouchableOpacity>
</View>
)
}
}
Then, create a PostList component that will be in charge of handling the logic for loading posts and handling like interactions:
class PostList extends Component {
/**
* As you can see, we are not storing "posts" as an array. Instead,
* we make it a JSON object. This allows us to access a post more concisely
* than if we stores posts as an array. For example:
*
* this.state.posts as an array
* findPost(postId) {
* return this.state.posts.find(post => post.id === postId)
* }
* findPost(postId) {
* return this.state.posts[postId]
* }
* a specific post by its "postId", you won't have to iterate
* through the whole array, you can just call "posts[postId]"
* to access it immediately:
* "posts": {
* "<post_id_1>": { "message": "", "uid": "", ... },
* "<post_id_2>": { "message": "", "uid": "", ... },
* "<post_id_3>": { "message": "", "uid": "", ... }
* }
* FlatList wants an array for its data property rather than an object,
* so we need to pass data={Object.values(this.state.posts)} rather than
* just data={this.state.posts} as one might expect.
*/
state = {
posts: {}
// Other states
}
renderItem = ({ item }) => {
const { date, message, uid, postId, other, props, here } = item
return (
<Post
date={date}
message={message}
uid={uid}
onPressLike={this.handleLikePost}
/>
)
}
handleLikePost = postId => {
let post = this.state.posts[postId]
const { liked, likeCount } = post
const newPost = {
...post,
liked: !liked,
likeCount: liked ? likeCount - 1 : likeCount + 1
}
this.setState({
posts: {
...this.state.posts,
[postId]: newPost
}
})
}
render() {
return (
<View style={{ flex: 1 }}>
<FlatList
data={Object.values(this.state.posts)}
renderItem={this.renderItem}
keyExtractor={({ item }) => item.postId}
/>
</View>
)
}
}
In summary:
1) Write a custom component (Post) for rendering each item in "FlatList"
2) Override the "shouldComponentUpdate" of the custom component (Post) function to tell the component when to update
Handle the "state of likes" in a parent component (PostList) and pass data down to each child
If you are testing on android than try turning off the developer mode. Or are you hitting some API and updating the post on the server and updating the like button in UI corresponding to the server response? If that is the case do tell me, I too have encountered this and I solved it. Also I have commented the second last line in your code which isn't needed.
// 1. FlatList
<FlatList
...
data={this.state.posts}
renderItem={this.renderPost}
...
/>
// 2. renderPost
renderPost({ item, index }) {
return (
<View style={someStyle}>
... // display other properties of the post
// Then display the "like" button
<Icon
name='favorite'
size={25}
color={item.liked ? 'red' : 'gray'}
containerStyle={someStyle}
iconStyle={someStyle}
onPress={() => this.onLikePost({ item, index })}
/>
...
</View>
);
}
// 3. onLikePost
likePost({ item, index }) {
let { posts } = this.state;
let targetPost = posts[index];
// Flip the 'liked' property of the targetPost
targetPost.liked = !targetPost.liked;
// Then update targetPost in 'posts'
// You probably don't need the following line.
// posts[index] = targetPost;
// Then reset the 'state.posts' property
this.setState({ posts });
}

React native navigator passing parent component to child

I don't know how to pass a reference to the TripList instance below to the AddTrip component. I need to do something like that to signal to TripList to refresh the data after adding a new trip.
In my render() method, inside <Navigator> I have:
if (route.index === 1) {
return <TripList
title={route.title}
onForward={ () => {
navigator.push({
title: 'Add New Trip',
index: 2,
});
}}
onBack={() => {
if (route.index > 0) {
navigator.pop();
}
}}
/>
} else {
return <AddTrip
styles={tripStyles}
title={route.title}
onBack={() => { navigator.pop(); }}
/>
}
However, when I call onBack() in AddTrip, after adding a trip, I want to call refresh() on TripList so the new trip is displayed. How best can I structure things to do that? I'm guessing I need to pass TripList somehow to AddTrip and then I can call refresh() there easily right before calling onBack().
This is not how React works. You don't pass instances of a component around, rather you pass the data to your component via props. And your component AddTrip should receive another props which is a function to call when adding a trip.
Let me illustrate this with a code example, this is not how your code should be in the end, but it'll illustrate how to contain the data outside of your components.
// Placed at the top of the file, not in a class or function.
let allTrips = [];
// Your navigator code.
if (route.index === 1) {
return <TripList
trips={allTrips}
title={route.title}
onForward={ () => {
navigator.push({
title: 'Add New Trip',
index: 2,
});
}}
onBack={() => {
if (route.index > 0) {
navigator.pop();
}
}} />
} else {
return <AddTrip
styles={tripStyles}
title={route.title}
onAdd={(tripData) => {
allTrips = [...allTrips, tripData];
}}
onBack={() => { navigator.pop(); }} />
}
As you can see, the logic about adding and finding the trips comes from the parent component, which is the navigator in this case. You will also note that we are reconstructing the content of allTrips, this is important as React is based on the concept of immutability.
You must have heard of Redux which is a system allowing all your components to discuss with a global store from which you fetch and save all your application state. It's a bit more complex that's why I did not use it as an example it.
I'll almost forget the most important! You will not need to signal to to your component that it needs refreshing, the magic of React should take care of it by itself!