How to make custom renderer interactive? (listen to component state update, and rerender) - react-native-render-html

I added checkbox into my custom renderer, but it doesn't listen to state update. Do you have any ideas how to make state working in custom renderer?
I have red, that in v5.0.1 you can use as a renderer -- react components. But I can't find details in docs.
const quizRenderer = (htmlAttribs, children) => {
const { type, quizid: quizId } = htmlAttribs
return <View key={`quiz-${quizId}`}>{children}</View>
}
const variantRenderer = (
htmlAttribs,
children,
convertedCSSStyles,
passProps
) => {
const { quizid: quizId, variantid: variantId, type } = htmlAttribs
console.log('userAnswers', userAnswers)
const handlePressQuizVariant = (quizId, variantId) => () => {
let quizAnswers = userAnswers[quizId] || []
//if this answer also been set, we remove it
// if there was clear cell -- we add it
if (quizAnswers.includes(variantId)) {
quizAnswers = quizAnswers.filter(elem => elem !== variantId)
} else {
quizAnswers.push(variantId)
}
setUserAnswers(userAnswers => {
const newUserAnswers = [...userAnswers]
newUserAnswers[quizId] = quizAnswers
return newUserAnswers
})
}
return (
<TouchableOpacity
key={`variant-${quizId}-${variantId}`}
onPress={handlePressQuizVariant(quizId, variantId)}
>
<Text>
<CheckBox
size={18}
checked={userAnswers[quizId].includes(variantId)}
{...(type === 'single'
? { checkedIcon: 'dot-circle-o', uncheckedIcon: 'circle-o' }
: {})}
containerStyle={{ margin: 0, padding: 0 }}
/>{' '}
{children}
</Text>
</TouchableOpacity>
)
}

Related

Reactjs - How use "return </Paper>" as code in my html?

I want to add into my code some tags <Paper> and </Paper>. But I didn't manage to do it. These two tags farmes my html code. But, when I runed my code, it show my them as string :
const test = () => {
const paperStyleTop = (nameMinistry) => {
if (nameMinistry === "justice"
) {
return `<Paper elevation={3} sx={{ ...(alertError && { border: "2px solid rgba(255,0,0,0.5)" }) }}>`
} else {
return `<Paper elevation={3}>`
}
}
const paperStyleBottom = () => {
return `</Paper>;`
}
const arrayMinistries = [
{
icon: "balance",
nameMinistry: "Justice",
id: "mJustice",
useStateFullName: ministers.justice.fullName
},
{
icon: "local_police",
nameMinistry: "Intérieur",
id: "mInterieur",
useStateFullName: ministers.interieur.fullName
}
]
return (
{arrayMinistries.map((ministry) => (
<Grid item}>
{paperStyleTop(ministry.nameMinistry)}
// Html code...
{paperStyleBottom()}
</Grid>
))}
)
export default test;
Could you explain to me how I can do to add these pieces of lines to my code ?
************** SOLUTION ***************
With that propose below it dit like this and that work :
const test = () => {
const paperProps = (nameMinistry) => {
const props = {
elevation: 3,
};
if (nameMinistry === "mJustice" ||
nameMinistry === "mInterieur" ||
nameMinistry === "mEducationNationale" ||
nameMinistry === "mSante" ||
nameMinistry === "mArmees" ||
nameMinistry === "mEconomieFinance"
) {
props.sx = { ...(alertError && { border: "2px solid rgba(255,0,0,0.5)" }) };
} else {
props.sx = {}
}
return props;
}
const arrayMinistries = [
{
icon: "balance",
nameMinistry: "Justice",
id: "mJustice",
useStateFullName: ministers.justice.fullName
},
{
icon: "local_police",
nameMinistry: "Intérieur",
id: "mInterieur",
useStateFullName: ministers.interieur.fullName
}
]
return (
{arrayMinistries.map((ministry) => (
<Grid item}>
<Paper {...paperProps(ministry.id)}>
// Html code...
</Paper>
</Grid>
))}
)
export default test;
This sounds like an XY problem to me. It appears that you want to pass specific props into your <Paper> component: what don't you object spread the props dictionary into it instead?
You can use useMemo() to memoize the props you want to spread, so that the object will be updated based on changes in the dependency array.
Example:
const test = () => {
const paperProps = useMemo(() => {
const props = {
elevation: 3,
};
if (ministry.nameMinistry === 'justice') {
props.sx = { ...(alertError && { border: "2px solid rgba(255,0,0,0.5)" }) };
}
return props;
}, [ministry.nameMinistry])
return (
<Grid item>
<Paper {...paperProps}>
{/* More content here */}
</Paper>
</Grid>
)
}

React Native screen does not update when object state changes

I am using react-native to build my app.
The main screen stores an object in the state:
const [menu, setMenu] = React.useState({name: "Pizza", toppings: {chicken: 2}})
and I display this state to the user as:
<Text>Qty: {menu.toppings.chicken}</Text>
I have a counter to update the state the loaginc to update the state is:
const handleChange = (action: string) => {
if (action === 'add') {
setMenu((prev) => {
prev.toppings.chicken += 1
return prev
})
} else if (action === 'subtract') {
setMenu((prev) => {
prev.calendar.booking -= 1
return prev
})
}
}
My function works correctly and changes the state to according to my needs(verified by console logging). However these changes are not reflexted in the <Text> where I am showing the quantity.
You should research more about Shallow compare:
How does shallow compare work in react
In your case, you can try this code:
const handleChange = (action: string) => {
if (action === 'add') {
setMenu((prev: any) => {
prev.toppings.chicken += 1;
return { ...prev };
});
} else if (action === 'subtract') {
setMenu((prev: any) => {
prev.calendar.booking -= 1;
return {...prev};
});
}
};
This is your solution, it gets complicated with nested objects :
const handleChange = () => {
setMenu((prevState) => ({
...prevState,
toppings: { chicken: prevState.toppings.chicken + 1 }
}));
};
Hope This Solution Help:
import React from 'react';
import {View, Text, TouchableOpacity} from 'react-native';
export default App = () => {
const [menu, setMenu] = React.useState({
name: 'Pizza',
toppings: {
value: 2,
},
});
React.useEffect(() => {
console.log('menu', menu);
}, [menu]);
const handleIncrement = () => {
setMenu(prev => ({
...prev,
toppings: {...prev.toppings, value: prev.toppings.value + 1},
}));
};
const handleDecrement = () => {
setMenu(prev => ({
...prev,
toppings: {...prev.toppings, value: prev.toppings.value - 1},
}));
};
return (
<View style={{flex: 1}}>
<View
style={{
alignItems: 'center',
flex: 1,
flexDirection: 'row',
justifyContent: 'space-evenly',
}}>
<TouchableOpacity onPress={handleDecrement}>
<Text style={{fontSize: 44}}>-</Text>
</TouchableOpacity>
<Text>{menu.toppings.value}</Text>
<TouchableOpacity onPress={handleIncrement}>
<Text style={{fontSize: 44}}>+</Text>
</TouchableOpacity>
</View>
</View>
);
};

How to pass down item from a flatlist to a grandchild?

I have a flatlist of items. When I pass the item to the renderItem component everything works perfectly fine. Then when I pass that exact same item to a child within my component responsible for rendering items there are problems.
Normally it works perfectly fine but if there are multiple list items and the one above it gets deleted, it loses proper functionality and becomes very buggy. I think the issue is because an item assumes a previous item's index for whatever reason the grandchild still thinks it is that item rather than it was a different item moving into that index.
My flatlist:
<FlatList
data={this.props.items}
extraData={this.props.items}
keyExtractor={(item, index) => index.toString()}
renderItem={({ item }) => {
return (
<TodoItem
todoItem={item}
/>
);
}}
/>
Then in the TodoItem this is how I pass item to the grandchild:
class TodoItem extends Component {
render() {
const todoItem = this.props.todoItem;
return (
<View>
<ItemSwipeRow
item={todoItem}
completeItem={this.props.deleteTodo}
>
Then in the itemSwipeRow this is how I receive the prop
import React, { Component } from 'react';
import { Animated, StyleSheet, View } from 'react-native';
import { RectButton } from 'react-native-gesture-handler';
import Swipeable from 'react-native-gesture-handler/Swipeable';
import { Ionicons, MaterialCommunityIcons } from '#expo/vector-icons';
import { connect } from 'react-redux';
import { openNotesModal, openDateModal } from '../actions';
const AnimatedIcon = Animated.createAnimatedComponent(Ionicons);
class ItemSwipeRow extends Component {
constructor(props) {
super(props);
this.item = props.item;
}
renderLeftActions = (progress, dragX) => {
const trans = dragX.interpolate({
inputRange: [0, 50, 100, 101],
outputRange: [-20, 0, 0, 1],
});
return (
<RectButton style={styles.leftAction}>
<AnimatedIcon
name='md-checkmark'
color='#28313b'
size={45}
style={[
styles.actionText,
{
transform: [{ translateX: trans }],
},
]}
/>
</RectButton>
);
};
renderRightAction = (action, name, color, x, progress) => {
const trans = progress.interpolate({
inputRange: [0, 1],
outputRange: [x, 0],
});
const pressHandler = () => {
action(this.item);
};
return (
<Animated.View style={{ flex: 1, transform: [{ translateX: trans }] }}>
<RectButton
style={[styles.rightAction, { backgroundColor: color }]}
onPress={pressHandler}
>
<MaterialCommunityIcons
name={name}
size={35}
color='#28313b'
/>
</RectButton>
</Animated.View>
);
};
renderRightActions = progress => (
<View style={styles.rightSwipeButtons}>
{this.renderRightAction(
this.props.openDateModal, 'calendar', '#B8B8F3', 192, progress
)}
{this.renderRightAction(
this.props.openNotesModal, 'pencil', '#F0A202', 128, progress
)}
{this.renderRightAction(
this.props.openDateModal, 'bell', '#db5461', 64, progress
)}
</View>
);
updateRef = ref => {
this.currItem = ref;
};
close = () => {
if (this.currItem !== null) { this.currItem.close(); }
};
onSwipeableLeftOpen = () => {
this.props.completeItem();
this.close();
}
onSwipeableRightWillOpen = () => {
console.log(this.item.text); //tried passing in item here but didn't
} //work, instead of console logging on call it did every item # start
// then none when it was suppose to log it.
render() {
const { children } = this.props;
const { item } = this.props;
return (
<Swipeable
ref={this.updateRef}
friction={2}
leftThreshold={70}
rightThreshold={40}
renderLeftActions={this.renderLeftActions}
renderRightActions={this.renderRightActions}
onSwipeableLeftOpen={this.onSwipeableLeftOpen}
onSwipeableRightWillOpen={this.onSwipeableRightWillOpen}
>
{children}
</Swipeable>
);
}
}
const styles = StyleSheet.create({
leftAction: {
flex: 1,
backgroundColor: '#82ff9e',
justifyContent: 'center',
},
rightAction: {
alignItems: 'center',
flex: 1,
justifyContent: 'center',
},
rightSwipeButtons: {
width: 192,
flexDirection: 'row'
}
});
export default connect(null, { openNotesModal, openDateModal })(ItemSwipeRow);
My console logs prove the right item isn't always being rendered. The deleting of the item works properly however if an item is deleted and there is something under it the item that was under it assumes it was the item that was just deleted.
Any help with this would be greatly appreciated and I can provide more code if needed.
the code for deleting an item:
action that's sent on left swipe–
export const removeTodo = (item) => {
return {
type: REMOVE_TODO,
id: item.id
};
};
reducer action goes to–
case REMOVE_TODO: {
const newList = state.todos.filter(item => item.id !== action.id);
for (let i = 0, newId = 0; i < newList.length; i++, newId++) {
newList[i].id = newId;
}
return {
...state,
todos: newList
};
}
You could use destructuring assignment to copy the object, rather than refering to the same object:
const { todoItem } = this.props
further reading: https://medium.com/#junshengpierre/javascript-primitive-values-object-references-361cfc1cbfb0

React Native Dynamic View Mapped from Array Does Not Update When Element Changes

I have a view in which I am dynamically adding Input components from React Native Elements. Ultimately, I need to validate the text typed in Input, and so, for now, I am trying to change the errorMessage prop from false to true upon onChangeText. Unfortunately, nothing I try changes the errorMessage even though the state component changes to true. I have a Snack, and, for convenience, my code follows:
import * as React from 'react'
import { ScrollView, StyleSheet, View } from 'react-native'
import { Button, Icon, Input, Text } from 'react-native-elements'
import * as Shortid from 'shortid'
export default class App extends React.Component {
constructor (props) {
super(props)
this.state = {
timeInputs: [],
validities: []
}
this.addTimeInput = this.addTimeInput.bind(this)
this.removeTimeInput = this.removeTimeInput.bind(this)
this.mutateValidities = this.mutateValidities.bind(this)
}
componentDidMount () {
this.addTimeInput()
}
addTimeInput () {
const identifier = Shortid.generate()
const timeInputs = this.state.timeInputs
const validities = this.state.validities
let isTimeValid = false
let time = ''
new Promise(
(resolve, reject) => {
resolve(
validities.push(
{ id: identifier, value: isTimeValid }
),
timeInputs.push(
<View key = { identifier }>
<View style = { styles.row }>
<Input
errorMessage = {
this.state.validities.filter(
validity => validity.id !== identifier
).value == true
? 'Valid'
: 'Invalid'
}
errorStyle = { styles.error }
onChangeText = {
value => {
time = value
this.mutateValidities(identifier, true)
console.log('TIME ' + identifier + ': ' + time)
}
}
placeholder = 'HH:MM AM/PM'
ref = { React.createRef() }
/>
<Icon
color = { colors.dark }
name = 'add-circle'
onPress = { () => this.addTimeInput() }
type = 'material'
/>
<Icon
color = { colors.dark }
name = 'remove-circle'
onPress = {
() => {
if (this.state.timeInputs.length > 1) {
this.removeTimeInput(identifier)
} else {
console.log('LENGTH: ' + this.state.timeInputs.length)
}
}
}
type = 'material'
/>
</View>
</View>
)
)
}
)
.then(
this.setState(
{
timeInputs: timeInputs,
validities: validities
}
)
)
.catch(
(reason) => {
console.log(
'Failed to create time-input because of the following: ' + reason
)
}
)
}
mutateValidities (key, value) {
this.setState(
{
validities: this.state.validities.map(
validity => validity.id === key
? {...validity, value: value}
: validity
)
}
)
}
removeTimeInput (key) {
this.setState(
{
timeInputs: this.state.timeInputs.filter(
timeInput => timeInput.key !== key
),
validities: this.state.validities.filter(
validity => validity.id !== key
)
}
)
}
render () {
return (
<ScrollView contentContainerStyle = { styles.container }>
<Text h4 style = { styles.title }>Time Inputs</Text>
{
this.state.timeInputs.map(
(value) => { return value }
)
}
<Button
buttonStyle = { styles.button }
onPress = {
() => this.state.validities.map(
validity => console.log(validity.id + validity.value)
)
}
title = 'Log Validities'
/>
</ScrollView>
)
}
}
const colors = {
dark: 'steelblue',
light: 'aliceblue',
medium: 'lightsteelblue',
error: 'firebrick'
}
const styles = StyleSheet.create(
{
button: {
backgroundColor: colors.dark,
margin: 5
},
container: {
alignItems: 'center',
backgroundColor: colors.light,
flex: 1,
justifyContent: 'center'
},
error: {
color: colors.error,
fontSize: 12,
margin: 5
},
row: {
alignItems: 'center',
flexDirection: 'row',
margin: 5,
width: '80%'
},
title: {
margin: 5
}
}
)
Due credit goes to McGregor (2017) for getting me this far, but I am still stuck.
Reference:
McGregor, L. (2017, October 2) Whats the best way to update an object in an array in ReactJS? [Stack Overflow answer]. Retrieved from https://stackoverflow.com/a/46518653/6084947
I believe my problem was that I was trying to trigger changes in the view that were too deeply nested. Even assuming the props were passed correctly, the view would not have updated when the state changed because React only compares props and state so far. Following the recommendation made in "The Power Of Not Mutating Data" (Facebook, 2019), I avoided these issues with shallow comparison by making the text-input a full React component. I also applied React Redux to manage the state in a central store instead of passing props back and forth, which I never could get to work even after abstracting the text-input.
The code is now spread out between too many different files to post here, but anyone interested can view it on GitHub or Snack.
Reference:
Facebook. (2019). Optimizing Performance [Documentation]. Retrieved from https://reactjs.org/docs/optimizing-performance.html#the-power-of-not-mutating-data

React hook logging a useState element as null when it is not

I have a method,
const handleUpvote = (post, index) => {
let newPosts = JSON.parse(JSON.stringify(mappedPosts));
console.log('mappedPosts', mappedPosts); // null
console.log('newPosts', newPosts); // null
if (post.userAction === "like") {
newPosts.userAction = null;
} else {
newPosts.userAction = "like";
}
setMappedPosts(newPosts);
upvote(user.id, post._id);
};
That is attached to a mapped element,
const mapped = userPosts.map((post, index) => (
<ListItem
rightIcon = {
onPress = {
() => handleUpvote(post, index)
}
......
And I have
const [mappedPosts, setMappedPosts] = useState(null);
When the component mounts, it takes userPosts from the redux state, maps them out to a ListItem and appropriately displays it. The problem is that whenever handleUpvote() is entered, it sees mappedPosts as null and therefore sets the whole List to null at setMappedPosts(newPosts);
What am I doing wrong here? mappedPosts is indeed not null at the point when handleUpvote() is clicked because.. well how can it be, if a mappedPosts element was what invoked the handleUpvote() method in the first place?
I tried something like
setMappedPosts({
...mappedPosts,
mappedPosts[index]: post
});
But that doesn't even compile. Not sure where to go from here
Edit
Whole component:
const Profile = ({
navigation,
posts: { userPosts, loading },
auth: { user, isAuthenticated },
fetchMedia,
checkAuth,
upvote,
downvote
}) => {
const { navigate, replace, popToTop } = navigation;
const [mappedPosts, setMappedPosts] = useState(null);
useEffect(() => {
if (userPosts) {
userPosts.forEach((post, index) => {
post.userAction = null;
post.likes.forEach(like => {
if (like._id.toString() === user.id) {
post.userAction = "liked";
}
});
post.dislikes.forEach(dislike => {
if (dislike._id.toString() === user.id) {
post.userAction = "disliked";
}
});
});
const mapped = userPosts.map((post, index) => (
<ListItem
Component={TouchableScale}
friction={100}
tension={100}
activeScale={0.95}
key={index}
title={post.title}
bottomDivider={true}
rightIcon={
<View>
<View style={{ flexDirection: "row", justifyContent: "center" }}>
<Icon
name="md-arrow-up"
type="ionicon"
color={post.userAction === "liked" ? "#a45151" : "#517fa4"}
onPress={() => handleUpvote(post, index)}
/>
<View style={{ marginLeft: 10, marginRight: 10 }}>
<Text>{post.likes.length - post.dislikes.length}</Text>
</View>
<Icon
name="md-arrow-down"
type="ionicon"
color={post.userAction === "disliked" ? "#8751a4" : "#517fa4"}
onPress={() => handleDownvote(post, index)}
/>
</View>
<View style={{ flexDirection: "row" }}>
<Text>{post.comments.length} comments</Text>
</View>
</View>
}
leftIcon={
<View style={{ height: 50, width: 50 }}>
<ImagePlaceholder
src={post.image.location}
placeholder={post.image.location}
duration={1000}
showActivityIndicator={true}
activityIndicatorProps={{
size: "large",
color: index % 2 === 0 ? "blue" : "red"
}}
/>
</View>
}
></ListItem>
));
setMappedPosts(mapped);
} else {
checkAuth();
fetchMedia();
}
}, [userPosts, mappedPosts]);
const handleDownvote = (post, index) => {
let newPosts = JSON.parse(JSON.stringify(mappedPosts));
if (post.userAction === "dislike") {
newPosts.userAction = null;
} else {
newPosts.userAction = "dislike";
}
setMappedPosts(newPosts);
downvote(user.id, post._id);
};
const handleUpvote = post => {
let newPosts = JSON.parse(JSON.stringify(mappedPosts));
console.log("mappedPosts", mappedPosts); // null
console.log("newPosts", newPosts); // null
if (post.userAction === "like") {
newPosts.userAction = null;
} else {
newPosts.userAction = "like";
}
setMappedPosts(newPosts);
upvote(user.id, post._id);
};
return mappedPosts === null ? (
<Spinner />
) : (
<ScrollView
refreshControl={
<RefreshControl
refreshing={false}
onRefresh={() => {
this.refreshing = true;
fetchMedia();
this.refreshing = false;
}}
/>
}
>
{mappedPosts}
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center"
}
});
Profile.propTypes = {
auth: PropTypes.object.isRequired,
posts: PropTypes.object.isRequired,
fetchMedia: PropTypes.func.isRequired,
checkAuth: PropTypes.func.isRequired,
upvote: PropTypes.func.isRequired,
downvote: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
auth: state.auth,
posts: state.posts
});
export default connect(
mapStateToProps,
{ fetchMedia, checkAuth, upvote, downvote }
)(Profile);
The reason why your current solution doesn't work is because you're rendering userPosts inside of the useEffect hook, which looks like it only runs once, ends up "caching" the initial state, and that's what you end up seeing in your handlers.
You will need to use multiple hooks to get this working properly:
const Profile = (props) => {
// ...
const [mappedPosts, setMappedPosts] = useState(null)
const [renderedPosts, setRenderedPosts] = useState(null)
useEffect(() => {
if (props.userPosts) {
const userPosts = props.userPosts.map(post => {
post.userAction = null;
// ...
})
setMappedPosts(userPosts)
} else {
checkAuth()
fetchMedia()
}
}, [props.userPosts])
const handleDownvote = (post, index) => {
// ...
setMappedPosts(newPosts)
}
const handleUpvote = (post) => {
// ...
setMappedPosts(newPosts)
}
useEffect(() => {
if (!mappedPosts) {
return
}
const renderedPosts = mappedPosts.map((post, index) => {
return (...)
})
setRenderedPosts(renderedPosts)
}, [mappedPosts])
return !renderedPosts ? null : (...)
}
Here's a simplified example that does what you're trying to do:
CodeSandbox
Also, one note, don't do this:
const Profile = (props) => {
const [mappedPosts, setMappedPosts] = useState(null)
useEffect(() => {
if (userPosts) {
setMappedPosts() // DON'T DO THIS!
} else {
// ...
}
}, [userPosts, mappedPosts])
}
Stay away from updating a piece of state inside of a hook that has it in its dependency array. You will run into an infinite loop which will cause your component to keep re-rendering until it crashes.
Let me use a simplified example to explain the problem:
const Example = props => {
const { components_raw } = props;
const [components, setComponents] = useState([]);
const logComponents = () => console.log(components);
useEffect(() => {
// At this point logComponents is equivalent to
// logComponents = () => console.log([])
const components_new = components_raw.map(_ => (
<div onClick={logComponents} />
));
setComponents(components_new);
}, [components_raw]);
return components;
};
As you can see the cycle in which setComponents is called, components is empty []. Once the state is assigned, it stays with the value logComponents had, it doesn't matter if it changes in a future cycle.
To solve it you could modify the necessary element from the received data, no components. Then add the onClick on the return in render.
const Example = props => {
const { data_raw } = props;
const [data, setData] = useState([]);
const logData = () => console.log(data);
useEffect(() => {
const data_new = data_raw.map(data_el => ({
...data_el // Any transformation you need to do to the raw data.
}));
setData(data_new);
}, [data_raw]);
return data.map(data_el => <div {...data_el} onClick={logData} />);
};

Resources