React Native rerendering UI after using array.map - reactjs

I am attempting to have an icon switch its visual when clicked (like a checkbox). Normally in react native I would do something like this:
const [checkbox, setCheckbox] = React.useState(false);
...
<TouchableHighlight underlayColor="transparent" onPress={() => {setCheckbox(!setCheckbox)}}>
{added ? <MaterialIcons name="playlist-add-check" size={40} />
: <MaterialIcons name="playlist-add" size={40} />}
</TouchableHighlight>
However I have made some changes, and now I can't seem to replicate this behavior. I am using AsyncStorage class to storage and get arrays of objects for display. For simplification, in the example below I removed the storage code, and the objects each have an 'id' and an 'added' attribute, which is essentially the boolean value of the checkbox.
I am now attempting to update the icon shown to the user whenever it is pressed. I know the function is being called, but it will not update the icon. I am using array.map to create the list of icons. I created a demo here, and the code is below: https://snack.expo.dev/#figbar/array-map-icon-update
const templateObject = {
id: 0,
added: false,
};
const templateObject2 = {
id: 1,
added: true,
};
export default function App() {
const [savedNumbers, setSavedNumbers] = React.useState([]);
React.useEffect(() => {
setSavedNumbers([templateObject,templateObject2]);
}, []);
const populateSavedNumbers = () =>
savedNumbers.map((num, index) => <View key={index}>{renderPanel(num.id,num.added)}</View>);
const updateNumber = (id) => {
let capturedIndex = -1;
for(var i = 0; i < savedNumbers.length; i += 1) {
if(savedNumbers[i].id === id) {
capturedIndex = i;
break;
}
}
let _tempArray = savedNumbers;
_tempArray[capturedIndex].added = !_tempArray[capturedIndex].added;
setSavedNumbers(_tempArray);
}
const renderPanel = (id:number, added:boolean) => {
return (
<View>
<TouchableHighlight underlayColor="transparent" onPress={() => {updateNumber(id);}}>
{added ? <MaterialIcons name="playlist-add-check" size={40} />
: <MaterialIcons name="playlist-add" size={40} />}
</TouchableHighlight>
</View>
);
}
return (
<View>
<View>buttons:</View>
<View>{populateSavedNumbers()}</View>
</View>
);
}

This is a common React pitfall where things don't re-render when it seems like they should. React does shallow comparisons between new and old states to decide whether or not to trigger a re-render. This means that, when declaring a variable to simply equal a state variable which is an object or an array, a re-render is not triggered since those two variables now reference the same underlying data structure.
In this case, you are setting _tempArray to reference the array savedNumbers rather than creating a new array. Therefore, React's shallow comparison comes back as "equal", and it doesn't believe that a re-render is necessary.
To fix this, change this line:
let _tempArray = savedNumbers;
to this:
let _tempArray = [...savedNumbers];

Related

Passing a button select choice back to previous screen

So after some research, I have learned how to make a button that will take the user to another screen, and provide them a text input where they can enter some words, then on pushing the done button take them back to the previous screen where what they typed will be displayed. But for my particular needs, I am trying to figure out how to instead of a text input have a selection of buttons, such as "large, medium, small" and have that button select the data that would be displayed instead, and return them to the previous page where it is displayed.
initial screen
function HomeScreen( route ) {
navigation = useNavigation();
React.useEffect(() => {
if (route.params?.post) {
}
}, [route.params?.post]);
return (
<View>
<Pressable
title="Create post"
onPress={() => navigation.navigate('CreatePost')}
>
<Text style={{ margin: 10 }}>Post: {route.params?.post}</Text>
</Pressable
</View>
);
}
button selection screen
function CreatePostScreen( route ) {
const navigation = useNavigation();
const [postText, setPostText] = React.useState('');
return (
<>
<Pressable
title="Done"
onPress={() => {
navigation.navigate({
name: 'Home',
params: { postText },
merge: true,
});
}}
>
<Text>
Large
</Text>
</Pressable>
</>
);
}
any insight is greatly appreciated.
You can pass the data in form of object
{post: postText,
buttonType: 'medium'}
For getting the data
React.useEffect(() => {
if (route.params?.post) {
var buttonType= route.params?.buttonType
}
}, [route.params?.post]);
You can store the button type in a variable or state
var buttonType = route.params?.buttonType
Or You can try with useState() hooks
const [buttonType, setButtonType]=useState("")
setButtonType(route.params?.buttonType)
The for using it just do the following
<Text>{buttonType}</Text>
Please follow the React-Documentation

how can I display the current ref value in my button?

I use useRef instead of useState. But my btn name not changing. Why ?
const data = useRef(null);
const handlePressCategory = (category: Categories) => {
data.current = category;
modalCategoriesRef.current?.close();
}
<Pressable style={[ButtonStyles.full_without, s.btn]} onPress={handleOpenModalCategories}>
<Text style={s.btnText}>{ data.current === null ? 'Kategorie auswählen' : data.current}</Text>
</Pressable>
I am very thankful for your help and answers. I dont know why its not displaying
Changing data with useRef doesn't trigger React to re-render your views. Think about it like some field in class that you can change without any reaction.
https://reactjs.org/docs/hooks-reference.html#useref
If you need re-render view when your ref have changes, just use useState. It's designed specially for it
Otherwise, if it's not possible, you can write some useForceRerender hook that will invoke render of your component. Something like that:
export const useForceRerender = () => React.useReducer((x) => x + 1, 0)[1];
const App = () => {
const data = useRef(null);
const forceRerender = useForceRerender();
const handlePressCategory = (category: Categories) => {
data.current = category;
modalCategoriesRef.current?.close();
forceRerender(); // <-- here
}
return (
<Pressable style={[ButtonStyles.full_without, s.btn]} onPress={handleOpenModalCategories}>
<Text style={s.btnText}>{ data.current === null ? 'Kategorie auswählen' : data.current}</Text>
</Pressable>
)
}

How to use useEffect inside a View in React Native

I want to use an useEffect inside a return (inside a Text that is inside multiple View to be exact) and from what I know, I must use {...} in order to say that what I write is some code. Howether I got a blank screen without errors and I don't know where is the issue with my code.
Here is the code:
const [pass, setPass] = useState(0);
...
return (
<View>
<FlatList
data={letter.description}
numColumns={2}
keyExtractor={(_, index) => index.toString()}
renderItem={({ item }) => {
if (pass >= letter.description?.length) {
useEffect(() => {
setPass((prev) => 0);
});
}
return (
<View>
<Text>
{letter.data[pass]}
{"\n"}
</Text>
<Text>
{letter.description[pass]}
{useEffect(() => {
setPass((prev) => prev + 1);
})}
{"\n"}
</Text>
</View>
);
}}
/>
</View>
letter is my data, but you can ignore it. I just keep it here to explain why I need the pass
Why use useEffect to setState?
just set the state :
if (pass >= letter.description?.length) { setPass((prev) => 0); }
You can use UseEffect to render your component when you want to render it.
More of this here : https://reactjs.org/docs/hooks-effect.html

Conditional Logic react native "Text strings must be rendered within a <Text> component."

Hi i am making an app in react native and i have a problem
let currentUserUID = firebase.auth().currentUser.uid;
const [partnership, setPartnership] = useState('');
useEffect(() => {
async function getUserInfo(){
let doc = await firebase
.firestore()
.collection('users')
.doc(currentUserUID)
.get();
let dataObj = doc.data();
setPartnership(dataObj.partnership)
}
getUserInfo();
})
console.log(partnership)
return(
<View style={{flex: 1}}>
{partnership === "Partner" || "1KPartner" || "BusinessPartner" &&
<Text>You have permission</Text>
}
<LessonHeader navigation={navigation}/>
<StyledBoxV3 style={{flex: 1}}>
<LessonMenu item={item} navigation={navigation} />
</StyledBoxV3>
</View>
)
};
const StyledBoxV3 = styled(StyledBox)`
padding: 0;
border-top-left-radius:0;
border-top-right-radius:0 ;
`
so when someone is Partner, 1KPartner or BusinessPartner i want to show or hide specific data but i get the error
"Error: Text strings must be rendered within a component."
The problem is in de conditional logic, if i delete it i dont have any problem. How can i make this work? I already tried to put a variable in my return and update it with a function in my useEffect but it doesn't work for me.
Your logic where you are comparing the value of the partnership is written wrong. I would suggest being classy and writing
{(partnership === "Partner" ||
partnership === "1KPartner" ||
partnership === "BusinessPartner") &&
<Text>You have permission</Text>}
Try updating the condition like this,
A helper function to determine the component to render based on the partnership state.
const renderPermissionText = (value: String) => {
switch (value) {
case 'Partner':
case '1KPartner':
case 'BusinessPartner':
return <Text>You have permission</Text>;
default:
return null;
}
};
Update the Layout to
...
<View style={{flex: 1}}>
{renderPermissionText(partnership)}
...
</View>

Unable to use ref to call the child method on a connect redux component

I want to call SingleCard child component methods in renderHiddenItem. I have assigned different ref name for each renderItem. But when I call this.name, it is undefined. Anything is wrong in this code? How can I achieve this?
<SwipeListView
data={this.state.listViewData}
renderItem={(data, i) => {
const name = 'childRef'+i
return (
<SingleCard
ref={component => this.name = component}
itm={data.item}
/>
);
}}
renderHiddenItem={(data, i) => {
const name = 'childRef'+i
return (
<TouchableOpacity onPress={ () => console.log(this.name)}>
<Text> h </Text>
</TouchableOpacity>
);
}
}}
/>
Update:
I want to trigger some action which is written in the singleCard component. Need to call that in renderHiddenItem.
Like this:
this.childRef0.someMethod
this.childRef1.someMethod
Instead of name you need to use the dynamic variable which can be done by using the bracket notation
<SwipeListView
data={this.state.listViewData}
renderItem={(data, i) => {
const name = 'childRef'+i
return (
<SingleCard
ref={component => this[name] = component}
itm={data.item}
/>
);
}}
renderHiddenItem={(data, i) => {
const name = 'childRef'+i
return (
<TouchableOpacity onPress={ () => console.log(this[name])}>
<Text> h </Text>
</TouchableOpacity>
);
}
}}
/>
Also when you use ref on a component which is created using an HOC for instance connect from react-redux, most of the libraries provide a method called getWrappedInstance to get the ref for the actual component instead of the connect component. You can use it like
this[name].getWrappedInstance()
but initially you need to set {withRef: true} as the fourth parameter to connect being used in SingleCard like
connect(mapStateToProps, mapDispatchToProps, null, { withRef: true })(SingleCard);
You can read more about it here
https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options

Resources