KeyboardAvoidingView - Reset height when Keyboard is hidden - javascript

I'm using React Natives KeyboardAvoidingView to set the height of my View when the Keyboard is shown. But when I close the the keyboard in the app, the height of the View is not changed back to it's original value.
<KeyboardAvoidingView behavior="height" style={styles.step}>
<View style={styles.stepHeader}>
// my content
</View>
</KeyboardAvoidingView>
The View with the red outline did take up the whole space before I opened and closed the keyboard.

A more detailed explanation on Nisarg's answer.
Create a key for the KeyboardAvoidingView in the constructor
constructor(props) {
this.state = {
keyboardAvoidingViewKey: 'keyboardAvoidingViewKey',
}
}
add listener on the keyboard's will/did hide (and remove it in the willUnmount)
import { KeyboardAvoidingView, Keyboard, Platform } from 'react-native'
componentDidMount() {
// using keyboardWillHide is better but it does not work for android
this.keyboardHideListener = Keyboard.addListener(Platform.OS === 'android' ? 'keyboardDidHide': 'keyboardWillHide', this.keyboardHideListener.bind(this));
}
componentWillUnmount() {
this.keyboardHideListener.remove()
}
update the keyboardAvoidingViewKey in the keyboardHideListener function, should be a new value each time (I used a timestamp) and use this key when rendering the KeyboardAvoidingView element.
keyboardHideListener() {
this.setState({
keyboardAvoidingViewKey:'keyboardAvoidingViewKey' + new Date().getTime()
});
}
render() {
let { keyboardAvoidingViewKey } = this.state
return (
<KeyboardAvoidingView behavior={'height'} key={keyboardAvoidingViewKey} style={...}>
...
</KeyboardAvoidingView>
)
}
Note:
Keep in mind that this will recreate the elements inside the KeyboardAvoidingView (i.e: will call their constructor function, I'm not quite sure why, I'll update the answer after deeper investigation), so you'll have to keep track of any state/prop values that might be overwritten
Update
After a much deeper investigation, I now know why the views are recreated once you change the key.
In order to truly understand why it happens, one must be familiar with how react-native dispatches the render commands to the native side, this particular explanation is pretty long, if it interests you, you can read my answer here. In short, react-native uses Reactjs to diff the changes that should be rendered, these diffs are then sent as commands to a component named UIManager, which sends imperative commands that translate into a layout tree, which changes the layout based on the diff commands.
Once you set a key on a component, reactjs uses this key to identify changes to said component, if this key changes, reactjs identifies the component as a completely new one, which in return sends the initial command to create said component, making all it's children to be created from scratch because there are identified as new elements in a new layout tree, deleting the old tree and creating a new one instead of just adjusting the diffs
If you would like, you can actually spy on these dispatched messages by adding the following code to your App.js file:
import MessageQueue from 'react-native/Libraries/BatchedBridge/MessageQueue'
const spyFunction = (msg) => {
console.log(msg);
};
MessageQueue.spy(spyFunction);
If you do that, you'll notice in the logs that each time the key changes, the command that is dispatched in return is createViews, which like stated above creates all the elements that are nested under said component.

Please give key to KeyboardAvoidingView and change when keyboard open and close so it will render and take height
<KeyboardAvoidingView behavior="height" style={styles.step} key={values}>

Wrap components in <KeyboardAvoidingView behavior="padding" style={styles}> on iOS and <View style={styles}> on android
render() {
const ScrollContainer: View | KeyboardAvoidingView =
this.renderDependingOnPlatform();
const scrollContainerParams: any = {};
if (isIOS)
scrollContainerParams.behavior = "padding";
return (
<ScrollContainer style={styles.container} {...scrollContainerParams}>
Scroll and other components
</ScrollContainer>
)}
/**
* Render scroll container depending on platform
* #returns {any}
*/
renderDependingOnPlatform() {
if (isAndroid())
return View;
else return KeyboardAvoidingView;
}

A simple workaround is to set the behavior property of the KeyboardAvoidingView to 'padding'. This avoids the issue of recalling the constructor function, which allows you to safely store information in state (say you have two Inputs and you want to store the value of the text in state even if the user collapses the keyboard in between clicking the two inputs).
This method may slightly alter the layout of the KeyboardAvoidingView's children, so be aware of that.

Related

Alter react component state properly

I'm working at a project in which I have to display graphs.
For displaying graphs I'm using vis.js in particular react-vis-network a implementation for using parts of vis.js in React with its stateful approaches.
Initial nodes and edges are loaded before my component is mounted and are passed as props for an initial state.
I attached two eventHandler one direct to a vis.js (the underlying DOM library) and the other at a decorator (button).
The desired/expected behaviour:
A node is removed by clicking either the node or the corresponding button.
Observed behavior:
Sometimes a node is removed and sometimes a node just disappears for a few ms and is reattached but without a decorator/button.
I already tried to start with an empty state and attaching the nodes,edges in componentDidMount() but I got the same result. I hope you can give me a hint.
BTW: Is the way I use to attach components a/the right way?
Every other help to improve my class is appreciated also
class MyNetwork extends Component {
constructor(props){
super(props);
let componentNodes = [];
for (let node of props.nodes){
componentNodes.push(this.createNode(node));
}
let componentEdges = [];
for (let edge of props.edges){
componentEdges.push(this.createEdge(edge));
}
this.state = {nodes:componentNodes,edges:componentEdges};
["_handleButtonClick"].forEach(name => {
this[name] = this[name].bind(this);
});
}
createNode(node){
const Decorator = props => {
return (
<button
onClick={() =>{this._handleButtonClick(props);}}
>
Click Me
</button>
);
};
node.decorator = Decorator;
return React.createElement(Node,{...node})
}
createEdge(edge){
return React.createElement(Edge,{...edge})
}
addNode(node){
this.setState({
nodes: [...this.state.nodes, this.createNode(node)]
})
}
_handleButtonClick(e) {
if(e){
console.log("clicked node has id:" +e.id);
this.removeNode(e.id);
}
}
onSelectNode(params){
console.log(params);
window.myApp.removeNode(params[0]);
}
removeNode(id) {
let array = [...this.state.nodes]; // make a separate copy of the array
let index = array.findIndex(i => i.props.id === id );
array.splice(index, 1);
this.setState({nodes: array});
}
render() {
return (
<div id='network'>
<Network options={this.props.options} onSelectNode={this.onSelectNode}>
{[this.state.nodes]}
{[this.state.edges]}
</Network>
</div>
);
}
}
export default MyNetwork
Before clicking node 2
After clicking node 2
Update 1
I created a live example at stackblitz which isn't working yet caused by other failures I make and can't find.
The components I use are:
Network
Node
Edge
Edge and Node are extending Module
I reworked my MyNetwork component according to some mistakes xadm mentioned.
Components (espacially dynamic) shouldn't be stored in state.
I implemented two new functions nodes() and edges() // line 15-41*
key prop should be used, too.
key is used now // line 18 + 32*
Passed props cannot be modified, you still have to copy initial data
into state. State is required for updates/rerendering.
line 9*
*line numbers in live example I mentioned above
Update 2
I reworked my code and now the life sample is working.
My hope is that I could use the native vis.js events and use them in MyNetwork or other Components I will write.
I read about using 3rd Party DOM event in this question can't figure out to adapt it for my particular case. Because I don't know how to attach the event handler to . Is this possible to do so I can use the event in other components?
Or should I open another question for this topic?
I see several possibilities of problems here.
<Decorator/> should be defined outside of <MyNetwork /> class. Click handler should be passed as prop.
Components (espacially dynamic) shouldn't be stored in state. Just render them in render or by rendering method (called from render). Use <Node/> components with decorator prop, key prop should be used, too.
Passed props cannot be modified, you still have to copy initial data into state. State is required for updates/rerendering. You probably need to remove edge(-es) while removing node.
Create a working example (on stackblitz?) if a problem won't be resolved.
It sounds like React is re-initializing your component when you are clicking a button. Maybe someone smarter than I am can figure out why that is happening...
But since no one has commented on this yet, one way I have handled these sorts of issues is to take the state management out of the display component. You say you are passing the nodes and edges via props from a parent component. You might consider moving the addNode, removeNode, createEdge, and other methods up to the parent component so that it is maintaining the state of the node/edge structure and your display component <MyNetwork/> is only displaying what it receives as props.
Perhaps this isn't an option in your app, but I generally use Redux to remove the state management from the components all together. I find it reduces situations like this where "who should own the state" isn't always clear.

List item won't change height in response to large text from accessibility being turned on

I'm currently working in React Native and I'm trying to make my list item components accessible to people with poor vision and might need bigger text, because of this I've ran into some issues with the text clipping or not showing entirely. What I'm trying to do is pretty straightforward, if the user turns on large text in accessibility then my event listener will pick up on it and using that information will determine the height of my list component when rendering. Unfortunately, it seems as though my app can never detect when the accessibility is on because in the console I get a lot of false and default height of 50 (when no large text is active). I'll attach some screenshots and then some code.
This is what my list view looks with large text, as you can see the text has gotten bigger but my individual list items aren't sizing correctly to fit the bigger text (should be 150, but still at 50).
This is what I get in the console..
Now here are some bits of code. Note, I omitted some code for brevity.
class NodeListItem extends PureComponent {
constructor() {
super();
this.state = {
largeTextEnabled: undefined,
};
this.itemPressed = this.itemPressed.bind(this);
}
componentDidMount() {
AccessibilityInfo.addEventListener(
'change',
this._handleAccessibilityToggled
);
AccessibilityInfo.fetch().done((isEnabled) => {
console.log(`Is enabled didMount: ${isEnabled}`);
this.setState({
largeTextEnabled: isEnabled,
});
});
}
componentWillUnmount() {
AccessibilityInfo.removeEventListener(
'change',
this._handleAccessibilityToggled
);
}
_handleAccessibilityToggled = (isEnabled) => {
this.setState({
largeTextEnabled: isEnabled,
});
};
render() {
const { displayName } = this.props.item;
const adjustForAccessibility = this.state.largeTextEnabled ? 150 : 50
console.log(`Here is height: ${adjustForAccessibility} and it is: ${this.state.largeTextEnabled}`);
console.log(`Height ${this.state.largeTextEnabled ? 150 : 50}`);
return (
<TouchableOpacity style={{ flex: 1, height: this.state.largeTextEnabled ? 150 : 50 }} onPress={this.itemPressed}>
<View style={styles.viewContainer}>
<Text style={CommonStyles.textDefaultBlackMedium} textAlign={'left'} allowFontScaling={true} numberOfLines={3} >{displayName}</Text>
{this.renderArrow()}
</View>
</TouchableOpacity>
);
}
}
I took inspiration from these 2 links, first and second. Essentially the idea is my app listens for change in accessibility and then updates the state with a boolean, then when rendering my component reads the value and adjusts height accordingly. Any idea why my list item components are not updating their size from 50 to 150 after turning accessibility on? I only ever see undefined or false in the console. Any help would be welcome.
EDIT: Resolved in the comments, issue was that the setting being altered was not part of the AccessibilityInfo API.
After a quick search I came across this issue which looks like the solution you are after:
https://github.com/facebook/react-native/issues/14725
No need to jump too deep into native code, you can almost always be certain someones already written it :P Especially with the recent focus on accessibility.
A Toggle function, when used with state should always reference the previous state and simple invert the boolean.
I'm not sure where your accessibility button is, but essentially your toggle function should look more like this:
_handleAccessibilityToggled = () => {
this.setState(prevState => {
return {
largeTextEnabled: !prevState.largeTextEnabled,
}
});
};
This way every click will flick the largeTextEnabled to the opposite state, I would also suggest removing event listeners for the button click and holding the state in a parent of both components if possible.
The main issue is that onChange you aren't actually passing isEnabled to the toggle function hence why it is always undefined or false.

React Component Flickers on Rerender

I'm pretty new to react and have been working on this new page for work. Basically, there's a panel with filter options which lets you filter objects by color. Everything works but I'm noticing the entire filter panel flickers when you select a filter.
Here are the areas functions in the filter component I think bear directly on the filter and then the parent component they're inserted into. When I had originally written this, the filter component was also calling re render but I've since refactored so that the parent handles all of that - it was causing other problems with the page's pagination functionality. naturally. and I think that's kind of my problem. the whole thing is getting passed in then rerendered. but I have no idea how to fix it. or what's best.
checks whether previous props are different from props coming in from parent and if so, creates copy of new state, calls a render method with those options. at this point, still in child component.
componentDidUpdate(prevProps, prevState) {
if (prevState.selectedColorKeys.length !== this.state.selectedColorKeys.length ||
prevState.hasMatchingInvitation !== this.state.hasMatchingInvitation) {
const options = Object.assign({}, {
hasMatchingInvitation: this.state.hasMatchingInvitation,
selectedColorKeys: this.state.selectedColorKeys
});
this.props.onFilterChange(options);
}
}
handles active classes and blocks user from selecting same filter twice
isColorSelected(color) {
return this.state.selectedColorKeys.indexOf(color) > -1;
}
calls to remove filter with color name so users can deselect with same filter button or if its a new filter, sets state by adding the color to the array of selected color keys
filterByColor(color) {
if (this.isColorSelected(color.color_name)) {
this.removeFilter(color.color_name);
return;
}
this.setState({
selectedColorKeys:
this.state.selectedColorKeys.concat([color.color_name])
});
}
creating the color panel itself
// color panel
colorOptions.map(color => (
colorPicker.push(
(<li className={['color-filter', this.isColorSelected(color.color_name) ? 'active' : null].join(' ')} key={color.key} ><span className={color.key} onClick={() => { this.filterByColor(color); }} /></li>)
)
));
parent component
callback referencing the filter child with the onFilterChange function
<ThemesFilter onFilterChange={this.onFilterChange} />
onFilterChange(filters) {
const { filterThemes, loadThemes, unloadThemes } = this.props;
unloadThemes();
this.setState({
filterOptions: filters,
offset: 0
}, () => {
filterThemes(this.state.filterOptions.selectedColorKeys, this.state.filterOptions.hasMatchingInvitation);
loadThemes(this.state.offset);
});
}
when I place break points, the general flow seems to be :
filterByColor is triggered in event handler passing in that color
active classes are added to the color, a filter tag for that color is generated and appended
componentDidMount takes in the previous props/state and compares it to the new props/state. if they don't match, i.e state has changed, it creates a copy of that object, assigning the new states of what's changed. passes that as props to onFilterChange, a function in the parent, with those options.
onFilterChange takes those options, calls the action method for getting new themes (the filtering actually happens in the backend, all I really ever need to do is update the page) and passes those forward. its also setting offset to 0, that's for the page's pagination functionality.
It looks like the problem might be around the componentDidUpdate function which, after setting breakpoints and watching it go through the steps from filterByColor to componentDidMount, that componentDidMount loops through twice, checking again if the colorIsSelected, and throughout all that the color panel pauses to re-render and you get a flicker.
Is it possible creating the copy is causing it? since it's being treated, essentially, as a new object that isColorSelected feels necessary to double check? any help you guys have would be much appreciated, this shit is so far over my head I can't see the sun.
Can you change
componentDidUpdate(prevProps, prevState)
with
componentWillUpdate(nextProps, nextState)

dynamically update device info react native

I am using react-native-device-info to get DeviceLocale or DeviceCountry. But is there a way that I can update Device-info with out restarting the app.
For Example, on device settings If my language is set to "English(US)", Device-info cam detect that but if I change the settings to "English(CA)",Device-Info is not able to capture that until I restart the app.
Any help is really appreciated.
How are you changing the settings of the language?
If it's built in...you can do something like setting the language as a state of the component and once call the function to change the language settings, you can setState and it'll update the component with the new value.
If you're talking about hopping out of the app and manually changing the language settings then maybe setting creating a setInterval to check the device-info will work.
Assuming you're defining the reference to DeviceInfo outside of the component's class definition, you could try moving it inside like this.
class MyComponent extends React.Component {
checkDeviceInfo() {
let DeviceInfo = require('react-native-device-info');
let locale = DeviceInfo.getDeviceLocale();
// do something with locale
}
render() {
return (
<TouchableOpacity onPress={this.checkDeviceInfo.bind(this)}>
<Text>Touch Here</Text>
</TouchableOpacity>
)
}
}
You could put this in your component so that it's called whenever you want to check for DeviceInfo changes.

How can I append react-native components to view without re-rendering?

Initially componentWillMount() will run a fetch() to an API endpoint & then save the javascript object to the redux store.
Now my problem is that when it comes to rendering the next set of components it re-renders all of them (meaning there's a little flash on the screen because of the rendering).
So essentially onScroll past a certain point it will run the same fetch() api call & grab a new list of javascript objects. It then grabs the data from the redux store & loops through it appending each new postComponent to the layout state.
handleScroll (event: Object) {
const offset = event.nativeEvent.contentOffset.y;
const screenHeight = Dimensions.get('window').height;
// Just for dev purposes until I find a proper way of determining half way down screen
if(offset >= screenHeight/2){
console.log("Halfway past...");
this.props.FeedActions.fetchFeed(this.props.feed.nextUrl, true);
}
}
render() {
var feed = this.props.feed;
if (!_.has(feed, 'posts')) {
return <ActivityIndicatorIOS />;
}
// Append more posts to state
for (var i = 0; i < _.size(feed.posts); i++) {
this.state.postComponents.push(
<PostComponent post={ feed.posts[i] } key={ "post_"+feed.posts[i].postId+Math.random() }/>
);
}
return (
<ScrollView key={Math.random()} onScroll={this.handleScroll.bind(this)}>
{ this.state.postComponents }
</ScrollView>
)
}
};
Is there a way around this? I thought react wouldn't re-render components that are already render, only the ones that are changed? But I guess in this case my components are all dynamic so that means they will be re-rendered.
The problem is in how you're creating your keys. What you want is a key that uniquely identifies that particular node, consistently, and doesn't change every render. Since you use Math.random() as part of your key, it changes the key every render, so react rebuilds that node. Try using postId without the random number trailing it.

Categories

Resources