I am trying to create an online library, which allows people to issue books. I want to limit the number of issued books to one, i.e a person can have only one issued book at a time. I have a firestore database for this. I read the number of books issued in ComponentDidMount(){}, and I have put an if-else statement to check how many books have been issued. For example, if someone has 1 book issued, it will display a different return(), if someone doesn't have 1 book issued, it will display another return(). It is working fine, but in the same component I have a button that can return the book (NOT return as in return(), return is used here to refer to UN-ISSUE THE BOOK), so I update the number of issued book of each user. But, I am not able to go the else statement. I guess I know why, I mean it checked the value once and entered the condition, but has no command to exit the conditions and check for other conditions. I figured that re-rendering the component should work, so please tell me how I can do this....
Also if there is any other solution to this problem in your mind, please share!
My Code (Sorry, I don't know how to add code in stackoverflow, new here)
import * as React from 'react';
import * as firebase from 'firebase';
import db from '../config'
import { StyleSheet, Text, View, Modal, ScrollView, TextInput , Image, TouchableOpacity, Alert, KeyboardAvoidingView} from 'react-native';
import { Header,Icon } from 'react-native-elements';
export default class AddItemScreen extends React.Component {
constructor(){
super()
this.state = {
itemName:'',
itemDescription:'',
userID : firebase.auth().currentUser.email,
itemID:'',
activeBarters:'',
userDocID : '',
itemDocID:'',
requestedBarter:'',
requestedBarterID:'',
requestedBarterDescription:'',
refresh:'',
}
}
componentDidMount = ()=> {
const userID = this.state.userID
db.collection('users').where('email','==',userID).get().then(snapshot => {
snapshot.forEach(doc => {
var data = doc.data()
this.setState({
activeBarters : data.activeBarters,
userDocID : doc.id
})
})
})
this.getRequestedBarter()
}
updateStatus = ()=> {
const docID = this.state.itemDocID
db.collection('items').doc(docID).update({
'item_status' : 'received'
})
}
addRequestedItem = ()=> {
const itemName = this.state.requestedBarter
const itemDescription = this.state.requestedBarterDescription
const itemID = this.state.requestedBarterID
const user = this.state.userID
db.collection('receivedItems').add({
'item_name' : itemName,
'item_description' : itemDescription,
'item_ID' : itemID,
'item_status' : 'received',
'userID' : user,
})
}
addReceivedNotification = ()=> {
const itemName = this.state.requestedBarter
const userID = this.state.userID
const message = "You received " + itemName + '. Congratulations!'
db.collection('allNotifications').add({
'notification_message' : message,
'senderID' : userID,
'targetedID' : userID,
'notification_status':'unread',
'itemName' : itemName,
})
}
addNotification = ()=> {
const itemName = this.state.itemName
const userID = this.state.userID
const message = 'You just added '+ itemName + ' to our item list! Thanks!'
db.collection('allNotifications').add({
'notification_message' : message,
'senderID' : userID,
'targetedID' : userID,
'notification_status':'unread',
'itemName' : itemName,
})
}
getRequestedBarter = ()=> {
const userID = this.state.userID
db.collection('items').where('userID','==',userID).get().then(snapshot => {
snapshot.forEach(doc => {
if(doc.data().item_status!='received'){
this.setState({
requestedBarter:doc.data().item_name,
requestedBarterDescription:doc.data().item_description,
itemDocID:doc.id,
requestedBarterID:doc.data().item_ID
})
}
})
})
}
addItem = async ()=> {
const name = this.state.itemName
const description = this.state.itemDescription
const itemID = Math.random().toString(36).substring(2)
const user = this.state.userID
if (name && description) {
db.collection('items').add({
"item_name" : name,
"item_description" : description,
"item_ID" : itemID,
"userID" : user,
"item_status" : "available"
})
this.setState({
itemName:'',
itemDescription:'',
})
Alert.alert('Item Added Succesfully')
}
else {
Alert.alert('Please fill Item Name and/or Description')
}
}
updateAcitveBarters = (number)=> {
const docID = this.state.userDocID
const numberArg = number
db.collection('users').doc(docID).update({
activeBarters : numberArg,
})
}
render(){
if (this.state.activeBarters === 1) {
return(
<View style = {styles.container}>
<Header
backgroundColor={'#222831'}
centerComponent={{
text: 'Add Items',
style: { color: '#32e0c4', fontSize: 20 },
}}
leftComponent = {
<Icon
name = 'bars'
type = 'font-awesome'
color = 'white'
onPress = {
()=>{
this.props.navigation.toggleDrawer()
}
}
></Icon>
}
rightComponent = {
<Icon
name = 'plus'
type = 'font-awesome'
color = '#15aabf'
></Icon>
}
></Header>
<Text style = {{color:'#eeeeee', textAlign : 'center', fontSize:18,marginTop:30}}>Sorry, You already have an active request!</Text>
<Text style = {{color:'#eeeeee', textAlign : 'center', fontSize:18,marginTop:30}}>{this.state.requestedBarter}</Text>
<Text style = {{color:'#eeeeee', textAlign : 'center', fontSize:18,marginTop:30}}>{this.state.requestedBarterDescription}</Text>
<TouchableOpacity
style={styles.button}
onPress = {()=>{
this.updateAcitveBarters(0)
this.updateStatus()
this.addReceivedNotification()
this.addRequestedItem()
}}
>
<Text style = {styles.buttonText}>I received the item</Text>
</TouchableOpacity>
</View>
)
}
else {
return (
<View style = {styles.container}>
<Header
backgroundColor={'#222831'}
centerComponent={{
text: 'Add Items',
style: { color: '#32e0c4', fontSize: 20 },
}}
leftComponent = {
<Icon
name = 'bars'
type = 'font-awesome'
color = 'white'
onPress = {
()=>{
this.props.navigation.toggleDrawer()
}
}
></Icon>
}
rightComponent = {
<Icon
name = 'plus'
type = 'font-awesome'
color = '#15aabf'
></Icon>
}
></Header>
<TextInput
style = {styles.textInput}
placeholder = 'Item Name'
onChangeText={
(text)=>{
this.setState({
itemName:text,
})
}
}
value = {this.state.itemName}
></TextInput>
<TextInput
style = {[styles.textInput,{ height:300}]}
placeholder = 'Item Description'
multiline = {true}
onChangeText={
(text)=>{
this.setState({
itemDescription:text,
})
}
}
value = {this.state.itemDescription}
></TextInput>
<TouchableOpacity style = {styles.button} onPress = {()=>{
this.addItem()
this.updateAcitveBarters(1)
this.addNotification()
this.props.navigation.navigate('Notifications')
}}>
<Text style = {styles.buttonText}>Add Item</Text>
</TouchableOpacity>
</View>
)
}
}
}
const styles = StyleSheet.create({
container: {
flex:1,
backgroundColor:'#393e46',
alignSelf:'center',
width:'100%'
},
title:{
backgroundColor:'#222831',
color:'#32e0c4',
fontSize:23,
padding:5,
alignContent:'center',
textAlign:'center',
},
buttonText:{
padding:10,
color:'#32e0c4',
alignSelf:'center',
textAlign:'center',
},
button:{
backgroundColor:'#222831',
width:100,
marginTop:40,
alignSelf:'center',
height:60,
alignItems:'center',
},
textInput:{
marginTop:30,
padding:10,
alignSelf:'center',
borderWidth:5,
borderColor:'#32e0c4',
width:300,
color:"#eeeeee",
},
})
Call your firebase API and re-initialize your state like you did in componentDidMount
updateApi() {
const userID = this.state.userID
db.collection('users').where('email','==',userID).get().then(snapshot => {
snapshot.forEach(doc => {
var data = doc.data()
this.setState({
activeBarters : data.activeBarters,
userDocID : doc.id
})
})
})
this.getRequestedBarter()
}
componentDidMount() {
this.updateApi();
}
updateAcitveBarters = (number)=> {
const docID = this.state.userDocID
const numberArg = number
db.collection('users').doc(docID).update({
activeBarters : numberArg,
})
this.updateApi();
}
Related
I am developing an app which allows the user to search for books and then display it in the search results. For displaying the results, I am using a FlatList with 3 columns and displaying the book cover and some basic information about the book.
I am storing the results from the API response in state without the comoponent. As more results are added, the memory consumption increases but the data is in JSON format, no images are store in state.
I have tried, using removeClippedSubviews and few other options that allow setting the window size but that has little to no difference on the memory usage.
Am I missing something here or is there a way to optimise this? Sample code is uploaded to this github repo
Here is the code snippet I am using:
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* #format
* #flow strict-local
*/
import type { Node } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import {
ActivityIndicator,
FlatList,
Platform,
SafeAreaView,
StatusBar,
StyleSheet,
useColorScheme,
View,
} from 'react-native';
import { Button, SearchBar, useTheme } from 'react-native-elements';
import { searchBooks } from './api/GoogleBooksService';
import HttpClient from './network/HttpClient';
import BookCard from './components/BookCard';
const searchParamsInitialState = {
startIndex: 1,
maxResults: 12,
totalItems: null,
};
let debounceTimer;
const debounce = (callback, time) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(callback, time);
};
const isEndOfList = searchParams => {
const { startIndex, maxResults, totalItems } = searchParams;
if (totalItems == null) {
return false;
}
console.log('isEndOfList', totalItems - (startIndex - 1 + maxResults) < 0);
return totalItems - (startIndex - 1 + maxResults) < 0;
};
const App: () => Node = () => {
const isDarkMode = useColorScheme() === 'dark';
const [isLoading, setIsLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [globalSearchResults, setGlobalSearchResults] = useState([]);
const [searchParams, setSearchParams] = useState(searchParamsInitialState);
let searchCancelToken;
let searchCancelTokenSource;
// This ref will be used to track if the search Term has changed when tab is switched
const searchRef = useRef();
const clearSearch = () => {
console.log('Clear everything!');
searchRef.current = null;
setGlobalSearchResults([]);
setSearchParams(searchParamsInitialState);
setIsLoading(false);
searchCancelTokenSource?.cancel();
searchCancelToken = null;
searchCancelTokenSource = null;
};
useEffect(() => {
debounce(async () => {
setIsLoading(true);
await searchGlobal(searchTerm);
setIsLoading(false);
}, 1000);
}, [searchTerm]);
/**
* Search method
*/
const searchGlobal = async text => {
if (!text) {
// Clear everything
clearSearch();
return;
}
setIsLoading(true);
try {
// Use the initial state values if the search term has changed
let params = searchParams;
if (searchRef.current !== searchTerm) {
params = searchParamsInitialState;
}
const { items, totalItems } = await searchBooks(
text,
params.startIndex,
params.maxResults,
searchCancelTokenSource?.token,
);
if (searchRef.current === searchTerm) {
console.log('Search term has not changed. Appending data');
setGlobalSearchResults(prevState => prevState.concat(items));
setSearchParams(prevState => ({
...prevState,
startIndex: prevState.startIndex + prevState.maxResults,
totalItems,
}));
} else {
console.log(
'Search term has changed. Updating data',
searchTerm,
);
if (!searchTerm) {
console.log('!searchTerm', searchTerm);
clearSearch();
return;
}
setGlobalSearchResults(items);
setSearchParams({
...searchParamsInitialState,
startIndex:
searchParamsInitialState.startIndex +
searchParamsInitialState.maxResults,
totalItems,
});
}
searchRef.current = text;
} catch (err) {
if (HttpClient.isCancel(err)) {
console.error('Cancelled', err.message);
}
console.error(`Error searching for "${text}"`, err);
}
setIsLoading(false);
};
const renderGlobalItems = ({ item }) => {
return <BookCard book={item} />;
};
const { theme } = useTheme();
return (
<SafeAreaView style={styles.backgroundStyle}>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
/>
<View style={styles.container}>
<SearchBar
showLoading={isLoading}
placeholder="Enter search term here"
onChangeText={text => {
setSearchTerm(text);
}}
value={searchTerm}
platform={Platform.OS}
/>
{isLoading && globalSearchResults.length <= 0 && (
<ActivityIndicator animating style={styles.loader} />
)}
{globalSearchResults.length > 0 && (
<FlatList
removeClippedSubviews
columnWrapperStyle={styles.columnWrapper}
data={globalSearchResults}
numColumns={3}
showsHorizontalScrollIndicator={false}
keyExtractor={item => item + item.id}
renderItem={renderGlobalItems}
ListFooterComponent={
<>
{!isLoading &&
!isEndOfList(searchParams) &&
searchParams.totalItems > 0 && (
<Button
type="clear"
title="Load more..."
onPress={async () => {
await searchGlobal(searchTerm);
}}
/>
)}
{isLoading && searchParams.totalItems != null && (
<ActivityIndicator
size="large"
style={{
justifyContent: 'center',
}}
color={theme.colors.primary}
/>
)}
</>
}
/>
)}
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
backgroundStyle: 'white',
container: {
height: '100%',
width: '100%',
},
columnWrapper: {
flex: 1,
},
loader: {
flex: 1,
justifyContent: 'center',
},
});
export default App;
There is something called PureComponent in react native. If you create FlatList as PureComponent, you can see lot of improvement.
It will not rerender items until data has been changed.
for example:
class MyList extends React.PureComponent {
}
For more reference check this
Can you try to chuck your array of list items into small sub-arrays, this package uses this mechanism https://github.com/bolan9999/react-native-largelist
The package has been praised by complex app teams including the Discord Mobile Team - https://discord.com/blog/how-discord-achieves-native-ios-performance-with-react-native
I have an Ul component which contains an array of objects with different values in it. Each item called TestCase in this list has a button which makes a restcall and updates its object. However not all TestItems need to be updated. Only those whose button are clicked. The state of this array is stored in a parent continer component called TestCaseContainer. My button will updates the state accordingly to the effected TestItem in the array, however. This causes the whole list to rerender. How can I only have the changed TestItems rendered, instead of having the entire ul rendered every time an element is updated. I read about using useMemo so the component can memoize the passed down props, however I don't know how to implement this properly.
How can I stop all the rerenders?
Regression.js - Holds all the state
const Testing = forwardRef((props,ref) => {
const templateTestItem = {id:0,formData:{date:'',env:'',assetClass:'',metric:'',nodeLevel:'',nodeName:'',testName:'',dataType:'',tradeId:''},results:[],isLoading:false}
const testCaseRef = useRef()
const [isRun, setIsRun] = useState(false)
const [testItems, setTestItems] = useState([ templateTestItem])
const [stats,setStats] = useState(null)
const addTestItem = () => {
const newIndex = testItems.length
// console.log(newIndex)
const templateTestItem = {id:newIndex,formData:{date:'',env:'',assetClass:'',metric:'',nodeLevel:'',nodeName:'',testName:'',dataType:'',tradeId:''},results:[],isLoading:false}
setTestItems([...testItems, templateTestItem])
}
const addUploadCases = (cases) => {
setTestItems([])
const UploadedItems = cases.map((item,index)=>{
return{
id:index,
formData:{
date:item['date'],
env:item['env'],
assetClass:item['asset_class'],
metric:item['metric'],
nodeLevel:item['node_level'],
nodeName:item['node_name'],
testName:item['test_name'],
dataType:item['dataType'],
tradeId:item['tradeId']
},
results:[]
}
})
setTestItems(UploadedItems)
}
const runAllTests = () => {
testCaseRef.current.runAll()
}
const clearTestCases = () => {
// console.log('Clear Test cases')
setTestItems([])
if (testItems.length == 0) {
setTestItems([templateTestItem])
}
}
const extractAllResults =()=>{
testCaseRef.current.extractAllResults()
}
const updateTestResults = useCallback( (result, index) => {
console.log('Index:', index)
setTestItems(prevObjs=>(prevObjs.map((item)=>{
let updatedItem = { ...item, results: result }
if(item.id==index) return updatedItem
return item
})))
},[])
return (
<div style={{ 'backgroundColor': '#1b2829', 'display': 'flex', }} className={styles.dashboard}>
<Grid>
<Row stretched style={{}} className={styles.buttonConsole}>
{<ButtonConsole addTest={addTestItem} addUploadCases={addUploadCases} runAllTests={runAllTests} clearTestCases={clearTestCases} extractAllResults={extractAllResults} />}
</Row>
<Row centered>
<TestRunStats stats={stats}/>
</Row>
<Row style={{ 'display': 'flex', 'flex-direction': 'column' }} ><TestCaseContainer countTestRunStats={countTestRunStats} updateTestResults={updateTestResults} isRun={isRun} ref={testCaseRef} testItems={testItems} /> </Row>
{/*
<Row></Row>
<Row></Row> */}
</Grid>
</div>
);
})
TestContainer.js
const TestCaseContainer = forwardRef((props, ref) => {
const testCaseRef = useRef([])
useImperativeHandle(ref, () => ({
extractAllResults: async () => {
const data = {
data:[],
summary:[]
}
testCaseRef.current.forEach(async (item, index) => {
try {
const workbook = item.extractAllResults()
const summary = workbook['summary']
workbook['data'].forEach(testData => {
data['data'].push(testData)
})
data['summary'].push(summary)
} catch (err) {
console.log(err)
}
})
await axios.post('http://localhost:9999/api/downloadresults', data).then(res => {
console.log('res', res)
const byteCharacters = atob(res.data);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: 'application/vnd.ms-excel' });
saveAs(blob, 'TestResults.xlsx')
})
},
runAll: () => {
testCaseRef.current.forEach(async (item, index) => {
await item.runAll()
})
}
}));
const runTestCase = async (date, env, nodeLevel, nodeName, assetClass, metric, dataType, tradeId, testName, key) => {
let testKey = key
console.log('FEtCHING', testKey)
try {
const params = {
nodeName,
date,
env,
nodeLevel,
assetClass,
metric,
dataType,
tradeId,
testName
}
const endpoint ={
sensitivities:'sensitivities'
}
if (metric == 'DELTA_SENSITIVITIES') {
const result = await axios.get('example.net/api/sensitivities', { params, }).then(response => {
console.log('response.data', response.data)
return response.data
})
if (result.data == 'none') {
toast.error(`${date}-${metric}-${nodeName} failed queried! No valutations for trades`, {
autoClose: 8000,
position: toast.POSITION.TOP_RIGHT
});
} else if (result.data != 'none') {
// setTestResult(result)
props.updateTestResults(result, testKey)
// updateTestResults(false,testKey,'isLoading')
toast.success(`${date}-${metric}-${nodeName} Successfully queried!`, {
autoClose: 8000,
position: toast.POSITION.TOP_RIGHT
});
}
// setTestResult(result.data)
} else {
await axios.get(`http://localhost:9999/api/metric/${metric}`, { params, }).then(response => {
if (response.data != 'none') {
props.updateTestResults(response.data, testKey)
toast.success(`${date}-${metric}-${nodeName} Successfully queried!`, {
autoClose: 8000,
position: toast.POSITION.TOP_RIGHT
});
} else {
toast.error(`${date}-${metric}-${nodeName} failed queried! No valutations for trades`, {
autoClose: 8000,
position: toast.POSITION.TOP_RIGHT
});
}
})
}
} catch (error) {
toast.error(`${date}-${metric}-${nodeName} failed queried! -${error
}`, {
autoClose: 8000,
position: toast.POSITION.TOP_RIGHT
});
}
}
return (
<Segment style={{ 'display': 'flex', 'width': 'auto', 'height': '100vh' }} className={styles.testCaseContainer}>
<div style={{ 'display': 'flex', }}>
</div>
<ul style={{overflowY:'auto',height:'100%'}} className='testItemContainer'>
{
// memoTestTwo
// testList
props.testItems.map((item, index) => {
let testName
if (item['formData']['testName'] == '') {
testName = `testRun-${index}`
} else {
testName = item['formData']['testName']
}
return <TestCase testResult={item['results']} runTestCase={runTestCase} isRun={props.isRun} ref={el => (testCaseRef.current[index] = el)} testKey={index} key={index} date={item['formData']['date']} env={item['formData']['env']} assetClass={item['formData']['assetClass']} metric={item['formData']['metric']} nodeLevel={item['formData']['nodeLevel']} nodeName={item['formData']['nodeName']} testName={testName} dataType={item['formData']['dataType']} tradeId={item['formData']['tradeId']} hierarchy={hierarchy} />
})
}
</ul>
</Segment>
)
})
TestCase.js - the individual item rendered from mapping!
const TestCase = forwardRef((props, ref) => {
const [isLoading, setIsLoading] = useState(false)
const inputRefs = useRef()
const outputRefs = useRef()
useImperativeHandle(ref, () => ({
extractAllResults: () => {
return outputRefs.current.extractAllResults();
},
runAll: () => {
inputRefs.current.runAll()
},
}));
const runSingleTestCase = async (date, env, nodeLevel, nodeName, assetClass, metric, dataType, tradeId, testName, key) => {
setIsLoading(true)
await props.runTestCase(date, env, nodeLevel, nodeName, assetClass, metric, dataType, tradeId, testName, key)
setIsLoading(false)
}
const convertDate = (date) => {
if (date) {
const newDate = date.split('/')[2] + '-' + date.split('/')[0] + '-' + date.split('/')[1]
return newDate
} else {
return date
}
}
return (
<Segment color='green' style={{ 'display': 'flex', 'flexDirection': 'column', }}>
<div style={{ 'display': 'flex', 'justify-content': 'space-between' }}>
<div style={{ 'display': 'flex', 'height': '30px' }}>
<Button
// onClick={props.deleteSingleTest(props.testKey)}
icon="close"
inverted
size="tiny"
color='red'
></Button>
</div>
<RegressionInput runSingleTestCase={runSingleTestCase} isRun={props.isRun} testKey={props.testKey} ref={inputRefs} nodeNames={props.hierarchy} runTestCase={props.runTestCase} date={convertDate(props.date)} testName={props.testName} env={props.env} assetClass={props.assetClass} metric={props.metric} nodeLevel={props.nodeLevel} nodeName={props.nodeName} dataType={props.dataType} tradeId={props.tradeId} />
<TestCheck pass={props.testResult ? props.testResult['CHECK'] : null} />
</div>
{
isLoading ? (<Loading type={'circle'} style={{ 'display': 'flex', 'flexDirecton': 'column', 'justify-content': 'center', 'align-items': 'center', 'marginTop': '50' }} inline />) : (
<RegressionOutput ref={outputRefs} testName={props.testName} testResult={props.testResult} />
)
}
</Segment>
)
})
This article might help you understand React rendering behavior better:
Blogged Answers: A (Mostly) Complete Guide to React Rendering Behavior
React's default behavior is that when a parent component renders, React will recursively render all child components inside of it!
To change that behavior you can wrap some of your components in React.memo(). So React will do a shallow compare on the props object and only re-render that if one of the top level properties of the props object has changed.
That's not always possible or recommended, especially if you are using props.children.
const TestItem = React.memo(({id,value}) => {
console.log(`Rendering TestItem ${id}...`);
return(
<div>TestItem {id}. Value: {value}</div>
);
});
const App = () => {
console.log("Rendering App...");
const [items,setItems] = React.useState([
{ id: 1, value: "INITIAL VALUE" },
{ id: 2, value: "INITIAL VALUE" },
{ id: 3, value: "INITIAL VALUE" },
]);
const testItems = items.map((item,index) =>
<TestItem key={index} id={item.id} value={item.value}/>
);
const updateTest = (index) => {
console.clear();
setItems((prevState) => {
const newArray = Array.from(prevState);
newArray[index].value = "NEW VALUE";
return newArray
});
};
return(
<React.Fragment>
<div>App</div>
<button onClick={()=>{updateTest(0)}}>Update Test 1</button>
<button onClick={()=>{updateTest(1)}}>Update Test 2</button>
<button onClick={()=>{updateTest(2)}}>Update Test 3</button>
<div>
{testItems}
</div>
</React.Fragment>
);
};
ReactDOM.render(<App/>, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
Without the React.memo() call. Every re-render of the App component would trigger a re-render in all of the TestItem component that it renders.
I have a component that is giving me the following issues:
TEST STEPS:
Login to Mobile.
On the Activity Feed, answer the first Get Involved question.
Scroll to the last Get Involved question and answer it.
Scroll back to the first Get Involved question that was answered.
EXPECTED RESULTS:
The answer for the first Get Involved question should still be selected.
ACTUAL RESULTS:
It is not selected.
So it seems the issue is the callback that updates some state on the parent, but the parent is not passing the yes/no prop to this component and it's unmounting to spare me rendering.
On the backend, in the database, the answers are being recorded, but it's not persisting in the UI. I noticed that the helper functions below, specifically the this.props.onPress() return undefined.
class GetInvolvedFeedCard extends PureComponent {
static propTypes = {
onPress: PropTypes.func.isRequired,
style: PropTypes.oneOfType([PropTypes.number, PropTypes.object]),
title: PropTypes.string.isRequired
};
constructor(props) {
super(props);
const helper = `${
this.props.title.endsWith("?") ? "" : "."
} Your NFIB preferences will be updated.`;
this.state = {
noSelected: false,
yesSelected: false,
helper
};
}
_accept = () => {
this.setState({ yesSelected: true, noSelected: false }, () => {
this.props.onPress(true);
});
};
_decline = () => {
this.setState({ noSelected: true, yesSelected: false }, () => {
this.props.onPress(false);
});
};
render() {
return (
<Card style={this.props.style}>
<View style={feedContentStyles.header}>
<View style={feedContentStyles.contentType}>
<Text style={feedContentStyles.title}>{"GET INVOLVED"}</Text>
</View>
</View>
<Divider />
<View style={feedContentStyles.content}>
<View style={styles.content}>
<Text style={styles.blackTitle}>
{this.props.title}
<Text style={styles.italicText}>{this.state.helper}</Text>
</Text>
</View>
<View style={styles.footer}>
<TouchableOpacity onPress={this._decline}>
<Text
style={[
styles.btnText,
styles.noBtn,
this.state.noSelected ? styles.selected : null
]}
>
{"NO"}
</Text>
</TouchableOpacity>
<TouchableOpacity onPress={this._accept}>
<Text
style={[
styles.btnText,
this.state.yesSelected ? styles.selected : null
]}
>
{"YES"}
</Text>
</TouchableOpacity>
</View>
</View>
</Card>
);
}
}
So it appears that the state is being managed by the parent component as opposed to Redux.
What is unclear is, whether or not it is unmounting the component that defines onPress. It sounds like it is.
Just scrolling, so there is an activity feed composed of cards some of them render boolean type questions
do you want to get involved? no yes
When a user selects either response, then scrolls down a certain amount and then scrolls back up to that same question, it's like the user never answered it. I noticed when the user selects NO, the this.props.handleUpdateGetInvolved({ involved: items }) from _handleGetInvolved function is not fired only when the user selects YES. Referring to this:
_handleGetInvolved = (response, entity) => {
if (response !== entity.IsSelected) {
const isTopic = entity.Category !== "GetInvolved";
const items = [
{
...entity,
IsSelected: response
}
];
if (isTopic) {
this.props.handleUpdateTopics({ topics: items });
} else {
this.props.handleUpdateGetInvolved({ involved: items });
}
}
};
the helper functions themselves for each answer always returns undefined inside the GetInvolvedFeedCard component:
_accept = () => {
this.setState({ yesSelected: true, noSelected: false }, () => {
this.props.onPress(true);
console.log(
"this is the accept helper function: ",
this.props.onPress(true)
);
});
};
_decline = () => {
this.setState({ noSelected: true, yesSelected: false }, () => {
this.props.onPress(false);
console.log(
"this is the decline helper function: ",
this.props.onPress(false)
);
});
};
render() {
return (
<Card style={this.props.style}>
<View style={feedContentStyles.header}>
<View style={feedContentStyles.contentType}>
<Text style={feedContentStyles.title}>{"GET INVOLVED"}</Text>
</View>
</View>
<Divider />
<View style={feedContentStyles.content}>
<View style={styles.content}>
<Text style={styles.blackTitle}>
{this.props.title}
<Text style={styles.italicText}>{this.state.helper}</Text>
</Text>
</View>
<View style={styles.footer}>
<TouchableOpacity onPress={this._decline}>
<Text
style={[
styles.btnText,
styles.noBtn,
this.state.noSelected ? styles.selected : null
]}
>
{"NO"}
</Text>
</TouchableOpacity>
<TouchableOpacity onPress={this._accept}>
<Text
style={[
styles.btnText,
this.state.yesSelected ? styles.selected : null
]}
>
{"YES"}
</Text>
</TouchableOpacity>
</View>
If I am not mistaken all this is being rendered overall by the ActivityFeed component:
const { height } = Dimensions.get("window");
export class ActivityFeed extends PureComponent {
static propTypes = {
displayAlert: PropTypes.bool,
feed: PropTypes.array,
fetchFeed: PropTypes.func,
getCampaignDetails: PropTypes.func,
handleContentSwipe: PropTypes.func,
handleUpdateGetInvoved: PropTypes.func,
handleUpdateTopics: PropTypes.func,
hideUndoAlert: PropTypes.func,
lastSwippedElement: PropTypes.object,
loading: PropTypes.bool,
navigation: PropTypes.object,
setSelectedAlert: PropTypes.func,
setSelectedArticle: PropTypes.func,
setSelectedEvent: PropTypes.func,
setSelectedSurvey: PropTypes.func.isRequired,
undoSwipeAction: PropTypes.func,
userEmailIsValidForVoterVoice: PropTypes.bool
};
constructor(props) {
super(props);
this.prompted = false;
this.state = {
refreshing: false,
appState: AppState.currentState
};
}
async componentDidMount() {
AppState.addEventListener("change", this._handleAppStateChange);
if (!this.props.loading) {
const doRefresh = await cache.shouldRefresh("feed");
if (this.props.feed.length === 0 || doRefresh) {
this.props.fetchFeed();
}
cache.incrementAppViews();
}
}
componentWillUnmount() {
AppState.removeEventListener("change", this._handleAppStateChange);
}
_handleAppStateChange = async appState => {
if (
this.state.appState.match(/inactive|background/) &&
appState === "active"
) {
cache.incrementAppViews();
const doRefresh = await cache.shouldRefresh("feed");
if (doRefresh) {
this.props.fetchFeed();
}
}
this.setState({ appState });
};
_keyExtractor = ({ Entity }) =>
(Entity.Key || Entity.Id || Entity.CampaignId || Entity.Code).toString();
_gotoEvent = event => {
cache.setRouteStarter("MainDrawer");
this.props.setSelectedEvent(event);
const title = `${event.LegislatureType} Event`;
this.props.navigation.navigate("EventDetails", { title });
};
_gotoSurveyBallot = survey => {
cache.setRouteStarter("MainDrawer");
this.props.setSelectedSurvey(survey);
this.props.navigation.navigate("SurveyDetails");
};
_gotoArticle = article => {
cache.setRouteStarter("MainDrawer");
this.props.setSelectedArticle(article);
this.props.navigation.navigate("ArticleDetails");
};
_onAlertActionButtonPress = async item => {
cache.setRouteStarter("MainDrawer");
await this.props.setSelectedAlert(item.Entity);
this.props.getCampaignDetails();
if (this.props.userEmailIsValidForVoterVoice) {
this.props.navigation.navigate("Questionnaire");
} else {
this.props.navigation.navigate("UnconfirmedEmail");
}
};
_onSwipedOut = (swippedItem, index) => {
this.props.handleContentSwipe(this.props, { swippedItem, index });
};
_handleGetInvolved = (response, entity) => {
if (response !== entity.IsSelected) {
const isTopic = entity.Category !== "GetInvolved";
const items = [
{
...entity,
IsSelected: response
}
];
if (isTopic) {
this.props.handleUpdateTopics({ topics: items });
} else {
this.props.handleUpdateGetInvoved({ involved: items });
}
}
};
renderItem = ({ item, index }) => {
const { Type, Entity } = item;
if (Type === "EVENT") {
return (
<SwippableCard onSwipedOut={() => this._onSwipedOut(item, index)}>
<EventFeedCard
style={styles.push}
mainActionButtonPress={() => this._gotoEvent(Entity)}
event={Entity}
/>
</SwippableCard>
);
}
if (["SURVEY_SURVEY", "SURVEY_BALLOT"].includes(Type)) {
return (
<SwippableCard onSwipedOut={() => this._onSwipedOut(item, index)}>
<SurveyBallotFeedCard
style={styles.push}
survey={Entity}
handleViewDetails={() => this._gotoSurveyBallot(Entity)}
/>
</SwippableCard>
);
}
if (Type === "SURVEY_MICRO") {
return (
<SwippableCard onSwipedOut={() => this._onSwipedOut(item, index)}>
<MicroSurvey style={styles.push} selectedSurvey={Entity} />
</SwippableCard>
);
}
if (Type === "ALERT") {
return (
<SwippableCard onSwipedOut={() => this._onSwipedOut(item, index)}>
<ActionAlertFeedCard
datePosted={Entity.StartDateUtc}
style={styles.push}
title={Entity.Headline}
content={Entity.Alert}
mainActionButtonPress={() => this._onAlertActionButtonPress(item)}
secondaryActionButtonPress={() => {
this.props.setSelectedAlert(Entity);
// eslint-disable-next-line
this.props.navigation.navigate("ActionAlertDetails", {
content: Entity.Alert,
id: Entity.CampaignId,
title: Entity.Headline
});
}}
/>
</SwippableCard>
);
}
if (Type === "ARTICLE") {
return (
<SwippableCard onSwipedOut={() => this._onSwipedOut(item, index)}>
<ArticleFeedCard
content={Entity}
style={styles.push}
mainActionButtonPress={() => this._gotoArticle(Entity)}
/>
</SwippableCard>
);
}
//prettier-ignore
if (Type === 'NOTIFICATION' && Entity.Code === 'INDIVIDUAL_ADDRESS_HOME_MISSING') {
return (
<MissingAddressCard
style={styles.push}
navigate={() => this.props.navigation.navigate('HomeAddress')}
/>
);
}
if (["PREFERENCE_TOPIC", "PREFERENCE_INVOLVEMENT"].includes(Type)) {
return (
<SwippableCard onSwipedOut={() => this._onSwipedOut(item, index)}>
<GetInvolvedFeedCard
style={styles.push}
title={Entity.DisplayText}
onPress={response => this._handleGetInvolved(response, Entity)}
/>
</SwippableCard>
);
}
return null;
};
_onRefresh = async () => {
try {
this.setState({ refreshing: true });
this.props
.fetchFeed()
.then(() => {
this.setState({ refreshing: false });
})
.catch(() => {
this.setState({ refreshing: false });
});
} catch (e) {
this.setState({ refreshing: false });
}
};
_trackScroll = async event => {
try {
if (this.prompted) {
return;
}
const y = event.nativeEvent.contentOffset.y;
const scrollHeight = height * 0.8;
const page = Math.round(Math.floor(y) / scrollHeight);
const alert = await cache.shouldPromtpPushNotificationPermissions();
const iOS = Platform.OS === "ios";
if (alert && iOS && page > 1) {
this.prompted = true;
this._openPromptAlert();
}
} catch (e) {
return false;
}
};
_openPromptAlert = () => {
Alert.alert(
"Push Notifications Access",
"Stay engaged with NFIB on the issues and activities you care about by allowing us to notify you using push notifications",
[
{
text: "Deny",
onPress: () => {
cache.pushNotificationsPrompted();
},
style: "cancel"
},
{
text: "Allow",
onPress: () => {
OneSignal.registerForPushNotifications();
cache.pushNotificationsPrompted();
}
}
],
{ cancelable: false }
);
};
_getAlertTitle = () => {
const { lastSwippedElement } = this.props;
const { Type } = lastSwippedElement.swippedItem;
if (Type.startsWith("PREFERENCE")) {
return "Preference Dismissed";
}
switch (Type) {
case "EVENT":
return "Event Dismissed";
case "SURVEY_BALLOT":
return "Ballot Dismissed";
case "SURVEY_SURVEY":
return "Survey Dismissed";
case "SURVEY_MICRO":
return "Micro Survey Dismissed";
case "ARTICLE":
return "Article Dismissed";
case "ALERT":
return "Action Alert Dismissed";
default:
return "Dismissed";
}
};
render() {
if (this.props.loading && !this.state.refreshing) {
return <Loading />;
}
const contentStyles =
this.props.feed.length > 0 ? styles.content : emptyStateStyles.container;
return (
<View style={styles.container}>
<FlatList
contentContainerStyle={contentStyles}
showsVerticalScrollIndicator={false}
data={this.props.feed}
renderItem={this.renderItem}
keyExtractor={this._keyExtractor}
removeClippedSubviews={false}
onRefresh={this._onRefresh}
refreshing={this.state.refreshing}
ListEmptyComponent={() => (
<EmptyState navigation={this.props.navigation} />
)}
scrollEventThrottle={100}
onScroll={this._trackScroll}
/>
{this.props.displayAlert && (
<BottomAlert
title={this._getAlertTitle()}
onPress={this.props.undoSwipeAction}
hideAlert={this.props.hideUndoAlert}
/>
)}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1
},
content: {
paddingHorizontal: scale(8),
paddingTop: scale(16),
paddingBottom: scale(20)
},
push: {
marginBottom: 16
}
});
const mapState2Props = ({
activityFeed,
auth: { userEmailIsValidForVoterVoice },
navigation
}) => {
return {
...activityFeed,
userEmailIsValidForVoterVoice,
loading: activityFeed.loading || navigation.deepLinkLoading
};
};
export default connect(mapState2Props, {
fetchFeed,
getCampaignDetails,
handleUpdateGetInvoved,
handleUpdateTopics,
setSelectedAlert,
setSelectedArticle,
setSelectedEvent,
setSelectedSurvey,
handleContentSwipe,
undoSwipeAction,
hideUndoAlert
})(ActivityFeed);
This is what the Get Involved card looks like:
So when the user clicks NO or YES, then scrolls that card away from view, the expectation is when they scroll that card back into view NO or YES, whichever one selected, should still be there.
Also, the answer selected only disappears when the user scrolls all the way to the bottom of the activity feed and then back up, but if the user only scrolls halfway through the activity feed cards and returns to this Get Involved card, the answer selected does not go away.
I believe the below SO article answer is exactly what is happening to me:
React Native - FlatList - Internal State.
So it seems the answer here would be to pass the answer from the application state as a prop and render based on that prop, and not the internal state of the card but I am not entirely sure what that looks like or how to put it together.
constructor(props) {
super(props);
// const helper = `${
// this.props.title.endsWith("?") ? "" : "."
// } Your NFIB preferences will be updated.`;
this.state = {
noSelected: false,
yesSelected: false,
helper
};
}
_accept = () => {
this.setState({ yesSelected: true, noSelected: false }, () => {
this.props.onPress(true);
});
};
_decline = () => {
this.setState({ noSelected: true, yesSelected: false }, () => {
this.props.onPress(false);
});
};
with:
this.state = {
// noSelected: false,
// yesSelected: false,
// helper
cardSelectedStatus: []
};
with the idea of then implementing cardSelectedStatus in my mapStateToProps
function mapStateToProps(state) {
return {cardSelectedStatus: state.};
}
but then I realized I am not sure what I am passing in as there are no action creators involved in this component, therefore no reducers
so can I use mapStateToProps if there is no action creator/reducer involved in this component?
utilizing mapStateToProps is the only way I know or its the way with the most experience I have in passing the user's input from the application state as a prop and render based on that prop, and not the internal state.
I can guess the problem. The problem is you are using something like FlatList. It will recreate the card every time you scroll it over screen, then scroll back.
So there are 2 ways to fix it:
Using ScrollView. It won't recreate cards
Move card states out of the Card component. Move them to List state. So in List state you will have something like:
this.state = {
cardSelectedStatus: [], // array [true, false, true, ...]. Mean card 0: selected, card 1: no selected,...
...
};
Then you can pass state to each card
So after several weeks of hammering away at this and posting the above question, here is what I finally honed in on:
By default the data for the preference is set to isSelected: false in the Redux state.
If I hit Yes, the Redux state is updated. This changes the isSelected to true.
If I hit No after hitting Yes, the Redux state is NOT updated, it stays isSelected: true.
So based on this, because isSelected does not have a neutral value, that is, starts out as false, which would equate to No, I cannot rely on Redux state for rendering the selection, which could have solved this.
Instead, in the interest of time, the only solution I could think of with much input from others includint tuledev, was to set <FlatList windowSize={500} />.
This is not an elegant solution but it will get FlatList to behave more like ScrollView in that preserve all the components so my component state will not get wiped out when a user scrolls to far away from it. This works without actually replacing it to ScrollView since there are properties and methods with FlatList that would not work by just dropping it into ScrollView same as it is in FlatList, for example, there is no renderItem property in ScrollView.
I am currently building a simple web using react with redux.
I have successfully integrated redux with app however I get 2 issue that I am unable to work out.
The first issue is that when the user apply the filter and then trigger the request the component keep rendering exceeding the limit.
The second issue is that when the user change the page the first click the request give a page markup but with the second click everything works.
These are my code so far.
action that take care of the request
import { BEGIN_FETCH_MOVIES, FETCHED_MOVIES, FETCH_FAILED_MOVIES } from '../constants';
import axios from 'axios';
//fetch movie
const searchQuery = (url) => {
return dispatch => {
//dispatch begin fetching
dispatch({
type : BEGIN_FETCH_MOVIES,
})
//make a get request to get the movies
axios.get(url)
.then((res) => {
//dispatch data if fetched
dispatch({type : FETCHED_MOVIES, payload : res.data});
})
.catch((err) => {
//dispatch error if error
dispatch({type : FETCH_FAILED_MOVIES});
});
}
//return the result after the request
}
export default searchQuery;
Main component
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { actionSearchMovie, actionSearchSerie } from '../actions'
import DisplayItemMovie from '../components/DisplayItemMovie';
import DisplayItemSerie from '../components/DisplayItemSerie';
import DrPagination from "../components/DrPagination";
import { Layout, Divider, Icon, Spin, Row } from 'antd';
//Home component
class Home extends Component {
constructor(){
super();
this.state = {
moviePage : 1,
seriePage : 1,
urlMovie : '',
urlSerie : ''
}
}
//make request before the render method is invoked
componentWillMount(){
//url
const discoverUrlMovies = 'https://api.themoviedb.org/3/discover/movie?api_key=72049b7019c79f226fad8eec6e1ee889&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=1';
//requests
this.fetchMovie(discoverUrlMovies);
}
fetchMovie = ( url ) => {
this.props.actionSearchMovie(url);
}
//handle pagination
handleChangePage = (page) =>{
let url = 'https://api.themoviedb.org/3/discover/movie?api_key=72049b7019c79f226fad8eec6e1ee889&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=' + page;
this.setState({
moviePage : page,
urlMovie : url
}, ()=> this.state);
this.fetchMovie(this.state.urlMovie);
}
//render
render() {
const movies = this.props.movies.results; //movies
let displayMovies; //display movies
const antIcon = <Icon type="loading" style={{ fontSize: 24 }} spin />; //spinner
//if movies and series is undefined, display a spinner
if(movies.results === undefined){
displayMovies = <Spin indicator={antIcon} />
}else {
//map through movies and series and then display the items
displayMovies = movies.results.map((movie) => {
return <DisplayItemMovie key = {movie.id} movie = {movie} />
});
}
return (
<div>
<div className='header'>
Home
</div>
<Divider />
<Layout style = {{paddingBottom : '1rem', margin : '0 auto' }}>
<h1 className = 'title'>Movie</h1>
<Row type = 'flex' style = {{flexWrap : 'wrap'}}>
{displayMovies}
</Row>
<DrPagination total = { movies.total_results } page = { this.handleChangePage } currentPage = { this.state.moviePage } /> </div>
)
}
};
const mapStateToProps = (state) => {
return{
movies : state.search_movies,
}
}
export default connect(mapStateToProps, { actionSearchMovie })(Home);
Filter component
i
mport React, { Component } from 'react';
import { Row, Col, Select, Button, Divider, InputNumber } from 'antd';
//official genres by movie db
const genres = [
{
"id": 28,
"name": "Action"
},
{
"id": 12,
"name": "Adventure"
}
];
class Filter extends Component {
constructor () {
super();
this.state = {
genre : [],
year : '',
rate : '',
filter : false,
}
}
//handle Genre
handleGenre = (genre) => {
console.log(genre);
this.setState({
genre
}, () => this.state);
}
//handle year
handleYear = (year) => {
this.setState({
year
}, () => this.state);
}
//handle Vote
handleVote = (rate) =>{
this.setState({
rate
}, ()=> this.state);
}
//handle on filter
handleFilter = () =>{
const { genre, year, rate } = this.state;
let url = 'https://api.themoviedb.org/3/discover/movie?api_key=72049b7019c79f226fad8eec6e1ee889&language=en-US&page=1';
if( genre.length === 0 && rate === "" && year === "" ){
alert('No filter is selected');
}else{
this.setState({
filter : true
});
let genreList = '';
if(genre.length > 0){
genreList = '&with_genres='+genre.join();
url += genreList;
}
if(year !== ""){
url += '&primary_release_year='+year;
}
if(rate !== ""){
url += '&vote_average='+rate;
}
//if filter === true pass the new url to the parent component
this.props.url(url);
}
}
render() {
const {filter} = this.state;
let header;
const displayGenres = genres.map((genre) => {
return (
<Select.Option key = { genre.id }>{genre.name}</Select.Option>
);
});
//
if(filter === false){
header = <h1>Top Movies</h1>
}else{
header = <h1>Custom Search</h1>
}
return (
<div>
{header}
<Divider />
<Row type = 'flex' align = 'middle' justify = 'center'>
<Col span = {14}>
<Row type = 'flex' align = 'middle' justify = 'center'>
<Col span = {5}>
<Select
mode = 'tags'
maxTagCount = {1}
style = {{width : '100%'}}
allowClear
placeholder="All Genre"
onChange = {this.handleGenre}
>
{displayGenres}
</Select>
</Col>
<Col span = {3}>
<Select
maxTagCount = {1}
style = {{width : '100%'}}
allowClear
placeholder="All Rate"
onChange = {this.handleVote}
>
<Select.Option value = '5'>>5</Select.Option>
<Select.Option value = '6'>>6</Select.Option>
<Select.Option value = '7'>>7</Select.Option>
<Select.Option value = '8'>>8</Select.Option>
<Select.Option value = '9'>>9</Select.Option>
</Select>
</Col>
<Col span = {3}>
<InputNumber
min = {1980}
max = {2019}
placeholder = 'All Year'
onChange = {this.handleYear}
/>
</Col>
</Row>
</Col>
<Col span = {5}>
<Button type = 'ghost' onClick = {this.handleFilter}>Filter</Button>
</Col>
</Row>
</div>
)
}
};
export default Filter;
Pagination component
import React, { Component } from 'react';
import { Pagination } from 'antd';
export default class DrPagination extends Component {
//on change page set the new page
handleChangePage = (page) =>{
this.props.page(page);
}
render() {
return (
<div style = {styles.container}>
<Pagination
current = {this.props.currentPage}
defaultCurrent = {1}
total = {this.props.total}
defaultPageSize = {20}
onChange = {this.handleChangePage}
size = 'small'
showQuickJumper
/>
</div>
)
}
};
const styles = {
container : {
width : '100%',
margin: '1rem',
display: 'flex',
justifyContent: 'center',
alignContent: 'center',
alignItems: 'center',
}
}
This is the data that I get when I try to change the page (it happen only for the first time).
Hi I have a problem in Draft-JS . I want to change the text like when user type ":)" it will change to emoji, But in this example I just want to change some word using "**" for testing. Somehow this.setState new editorstate in handleInput doesnt change the state.
import React, {Component} from 'react';
import {Editor, EditorState,getDefaultKeyBinding,moveFocusToEnd, KeyBindingUtil,getContentStateFragment, SelectionState, Modifier, CompositeDecorator} from 'draft-js';
const storage = {
"Abm7" : "Abminorseventh"
}
export default class MyEditor extends Component {
constructor(props) {
super(props);
this.state = { editorState: EditorState.createEmpty(), lastOffset:0 };
this.focus = () => this.refs.editor.focus();
this.logState = () => console.log(this.state.editorState.toJS());
}
onChange(editorState) {
this.setState({editorState});
}
handleBeforeInput(e) {
if(e === ' ') {
const { editorState } = this.state;
const content = editorState.getCurrentContent();
const selection = editorState.getSelection();
const end = selection.getEndOffset();
const block = content.getBlockForKey(selection.getAnchorKey());
const word = block.getText();
const result = word.slice(this.state.lastOffset, selection.getEndOffset());
const newSelection = new SelectionState({
anchorKey: block.getKey(),
anchorOffset: 0,
focusKey: block.getKey(),
focusOffset: 2
})
const contentReplaced = Modifier.replaceText(
content,
newSelection,
"**")
const editorStateModified = EditorState.push(
editorState,
contentReplaced,
'replace-text'
);
this.setState({lastOffset: selection.getEndOffset(), editorState:editorStateModified})
return true;
}else {
return false;
}
}
prompt(e) {
e.preventDefault();
const {editorState} = this.state;
const content = editorState.getCurrentContent();
const selection = editorState.getSelection();
const block = editorState.getCurrentContent().getBlockForKey(selection.getAnchorKey());
let text = block.getText().slice(selection.getStartOffset(), selection.getEndOffset());
const contentReplaced = Modifier.replaceText(
content,
selection,
storage[text])
const editorStateModified = EditorState.push(
editorState,
contentReplaced,
'replace-text'
);
console.log(editorStateModified.getCurrentContent())
this.setState({editorState:editorStateModified})
}
render() {
const styles = {
root: {
fontFamily: '\'Helvetica\', sans-serif',
padding: 20,
width: 600,
},
editor: {
border: '1px solid #ccc',
cursor: 'text',
minHeight: 80,
padding: 10,
},
button: {
marginTop: 10,
textAlign: 'center',
},
buttons: {
marginBottom: 10,
},
};
return (
<div style={styles.root}>
<div style={styles.buttons}>
<button
onMouseDown={(e)=>{this.prompt(e)}}
style={{marginRight: 10}}>
Change word
</button>
</div>
<div style={styles.editor} onClick={this.focus}>
<Editor
editorState={this.state.editorState}
onChange={(e)=>{this.onChange(e)}}
handleBeforeInput={(e)=>{this.handleBeforeInput(e)}}
placeholder="Enter some text..."
ref="editor"
/>
</div>
<input
onClick={this.logState}
style={styles.button}
type="button"
value="Log State"
/>
</div>
);
}
}
The function handleBeforeInput should return "handled", if you want for you changes to be applied. And it should be returned to Editor component, so you need to pass the function to the Editor component as it is, like this:
<Editor
...
handleBeforeInput={this.handleBeforeInput}
...
/>
Reference: https://draftjs.org/docs/api-reference-editor#handlebeforeinput