Reinitialization of #ion-phaser/react function component removes canvas from dom - javascript

I have a problem to reinitialize the ion-phaser function component in a parent component. It works fine by reinitilization just as a class component. Bellow are the two examples to displays which works and which not.
Here is my parent render function:
render() {
return(
<>
{this.state.visible && <IonComponent />}
</>
)
}
Here is the Ion-Phaser function component (this doesn't work):
let game = { ..here comes the Phaser game logic }
const IonComponent = () => {
const [initialize, setInitialize] = useState(false);
useEffect(() => {
if (game?.instance === undefined) {
setInitialize(true);
}
}, [initialize]);
return (
<>
{ initialize && <IonPhaser game={game} initialize={initialize} />}
</>
)
}
export default IonComponent;
Here is the Ion-Phaser class component (this works):
class IonComponent extends React.Component {
state = {
initialize: true,
game: { ..here comes the Phaser game logic }
}
render() {
const { initialize, game } = this.state
return (
<IonPhaser game={game} initialize={initialize} />
)
}
}
export default IonComponent;
When I switch in the parent component the state.visible at the first render to true, it initiate the child IonPhaser component without any problems. But after the state.visible switch once to false and then again back to true, the function component will not reinitialize and it removes the canvas from the dom. The class component however works fine.
Is this a persistent bug in Ion-Phaser by function component or am I doing something wrong?

Make sure you're keeping track of the state of your game using React's useState() hook or something similar.
For what it's worth, I also ran into some issues while trying to get the IonPhaser package working. To get around these issues, I've published an alternative way to integrate Phaser and React here that (I think) is more straightforward.
It works mostly the same way, but it doesn't come with as much overhead as the IonPhaser package.
npm i phaser-react-tools
And then you can import the GameComponent into your App.js file like so:
import { GameComponent } from 'phaser-react-tools'
export default function App() {
return (
<GameComponent
config={{
backgroundColor: '000000',
height: 300,
width: 400,
scene: {
preload: function () {
console.log('preload')
},
create: function () {
console.log('create')
}
}
}}
>
{/* YOUR GAME UI GOES HERE */}
</GameComponent>
)
}
It also comes with hooks that let you emit and subscribe to game events directly from your React components. Hope this helps, and please get in touch if you have feedback.

Related

How to check some or one element did mount in React?

I need to know when Hidden Element did mount.
I use ref to check it did mount and control this element.
And use componentDidUpdate to check when Hidden Element did mount.
But use componentDidUpdate in a big project, some elements often trigger componentDidUpdate.
I'm afraid the efficiency will be bad.
Is there another way for me to know when Hidden Element did mount?
Appreciate your help.
In addition, why I need to know it because I need to use a Radium package to build the animation.
When the 'someState' is true, I will auto play the animation for the element.
I use the style animation-play-state : 'running'.
This animation will break in the safari, but it is okay that users visit it for the first time.
When the users refresh safari and have a cache, the users visit it again causing the animation can't autoplay.
So I set animation-play-state : paused.
When I confirm the element did mount, I will use ref change animation-play-state to running.
I find an issue with this problem.
Link: https://github.com/FormidableLabs/radium/issues/912
My sudo code.
import React from "react";
const initialState = {
someState: false
};
class App extends React.Component {
constructor(props) {
super(props);
this.state = initialState;
this.hiddenElement = null;
}
componentDidMount() {
console.log("componentDidMount");
}
componentDidUpdate() {
console.log("componentDidUpdate");
if (this.hiddenElement !== null) console.log("hiddenElement did Mount");
// I will change the properties of this.hiddenElement, or others in the next steps.
}
render() {
const { someState } = this.state;
return (
<div className="App">
<button onClick={() => this.setState({ someState: true })}>
Click Me
</button>
{someState && (
<div ref={r => (this.hiddenElement = r)}>Hidden Element</div>
)}
</div>
);
}
}
export default App;
Okay, so if you're using an external package and need to manipulate the element based on a state change, then you'll have to basically use componentDidUpdate().
Only worry about the performance when it becomes a bottleneck (after profiling things, etc.).
Since setting a ref doesn't cause componentDidUpdate (it's not a bit of state), you may wish to refactor the animation-mutating method to something like this (note how the update...() method is called in the ref callback).
class App extends React.Component {
constructor(props) {
super(props);
this.state = { someState: false };
this.hiddenElement = null;
}
updateElementAnimation() {
if (!this.hiddenElement) return; // not mounted yet
if (this.state.someState) {
this.hiddenElement.something();
} else {
this.hiddenElement.somethingElse();
}
}
componentDidUpdate() {
this.updateElementAnimation();
}
render() {
const { someState } = this.state;
return (
<div className="App">
<button onClick={() => this.setState({ someState: true })}>Click Me</button>
{someState && (
<div
ref={r => {
this.hiddenElement = r;
this.updateElementAnimation();
}}
>
Hidden Element
</div>
)}
</div>
);
}
}
You can check inside your componentDidUpdate the value of your someState. If this is true, then you are sure that the element you need is rendered, as componentDidUpdate is invoked after an update occurs (thus after the render method).
...
componentDidUpdate() {
if (this.state.someState) {
// your element is rendered, do what you need
}
}
...

How to avoid rerendering camera in Android?

So the thing is im using react-native-qrcode-scanner and when I switch between tabs in my app, the QR scanner gets black in Android. I read its because in Android the components do not unmount. I had to add an isFocused if statement but its causing the whole thing to rerender and its a horrible user experience. Is there a way to make this better without having the if statement? Thanks!
import { withNavigationFocus } from 'react-navigation';
class ScannerScreen extends Component {
...
const { isFocused } = this.props
...
{isFocused ?
<QRCodeScanner
showMarker={true}
vibrate={false}
ref={(camera) => {this.state.scanner = camera}}
cameraStyle={{overflow: 'hidden', height: QRHeight}}
onRead={read}
bottomContent={<BottomQRScanner/>}
/>
:
null
}
}
export default withNavigationFocus(ScannerScreen)
Okay You can add this in your componentdidmount/componentwillmount && remove listner in componentwillunmount or useeffecthook
useEffect(() => {
const unsubscribe = navigation.addListener('willFocus', () => {
//Your function that you want to execute
});
return () => {
unsubscribe.remove();
};
});
Or in newer version just do this
import {NavigationEvents} from 'react-navigation';
with this
<NavigationEvents onDidFocus={() => console.log('I am triggered')} />
onDidFocus event will be get called whenever the page comes to focus .

Passing data up through nested Components in React

Prefacing this with a thought; I think I might require a recursive component but that's beyond my current ability with native js and React so I feel like I have Swiss cheese understanding of React at this point.
The problem:
I have an array of metafields containing metafield objects with the following structure:
{
metafields: [
{ 0:
{ namespace: "namespaceVal",
key: "keyVal",
val: [
0: "val1",
1: "val2",
2: "val3"
]
}
},
...
]
}
My code maps metafields into Cards and within each card lives a component <MetafieldInput metafields={metafields['value']} /> and within that component the value array gets mapped to input fields. Overall it looks like:
// App
render() {
const metafields = this.state.metafields;
return (
{metafields.map(metafield) => (
<MetafieldInputs metafields={metafield['value']} />
)}
)
}
//MetafieldInputs
this.state = { metafields: this.props.metafields}
render() {
const metafields = this.state;
return (
{metafields.map((meta, i) => (
<TextField
value={meta}
changeKey={meta}
onChange={(val) => {
this.setState(prevState => {
return { metafields: prevState.metafields.map((field, j) => {
if(j === i) { field = val; }
return field;
})};
});
}}
/>
))}
)
}
Up to this point everything displays correctly and I can change the inputs! However the change happens one at a time, as in I hit a key then I have to click back into the input to add another character. It seems like everything gets re-rendered which is why I have to click back into the input to make another change.
Am I able to use components in this way? It feels like I'm working my way into nesting components but everything I've read says not to nest components. Am I overcomplicating this issue? The only solution I have is to rip out the React portion and take it to pure javascript.
guidance would be much appreciated!
My suggestion is that to out source the onChange handler, and the code can be understood a little bit more easier.
Mainly React does not update state right after setState() is called, it does a batch job. Therefore it can happen that several setState calls are accessing one reference point. If you directly mutate the state, it can cause chaos as other state can use the updated state while doing the batch job.
Also, if you out source onChange handler in the App level, you can change MetafieldInputs into a functional component rather than a class-bases component. Functional based component costs less than class based component and can boost the performance.
Below are updated code, tested. I assume you use Material UI's TextField, but onChangeHandler should also work in your own component.
// Full App.js
import React, { Component } from 'react';
import MetafieldInputs from './MetafieldInputs';
class App extends Component {
state = {
metafields: [
{
metafield:
{
namespace: "namespaceVal",
key: "keyVal",
val: [
{ '0': "val1" },
{ '1': "val2" },
{ '2': "val3" }
]
}
},
]
}
// will never be triggered as from React point of view, the state never changes
componentDidUpdate() {
console.log('componentDidUpdate')
}
render() {
const metafields = this.state.metafields;
const metafieldsKeys = Object.keys(metafields);
const renderInputs = metafieldsKeys.map(key => {
const metafield = metafields[key];
return <MetafieldInputs metafields={metafield.metafield.val} key={metafield.metafield.key} />;
})
return (
<div>
{renderInputs}
</div>
)
}
}
export default App;
// full MetafieldInputs
import React, { Component } from 'react'
import TextField from '#material-ui/core/TextField';
class MetafieldInputs extends Component {
state = {
metafields: this.props.metafields
}
onChangeHandler = (e, index) => {
const value = e.target.value;
this.setState(prevState => {
const updateMetafields = [...prevState.metafields];
const updatedFields = { ...updateMetafields[index] }
updatedFields[index] = value
updateMetafields[index] = updatedFields;
return { metafields: updateMetafields }
})
}
render() {
const { metafields } = this.state;
// will always remain the same
console.log('this.props', this.props)
return (
<div>
{metafields.map((meta, i) => {
return (
<TextField
value={meta[i]}
changekey={meta}
onChange={(e) => this.onChangeHandler(e, i)}
// generally it is not a good idea to use index as a key.
key={i}
/>
)
}
)}
</div>
)
}
}
export default MetafieldInputs
Again, IF you out source the onChangeHandler to App class, MetafieldInputs can be a pure functional component, and all the state management can be done in the App class.
On the other hand, if you want to keep a pure and clean App class, you can also store metafields into MetafieldInputs class in case you might need some other logic in your application.
For instance, your application renders more components than the example does, and MetafieldInputs should not be rendered until something happened. If you fetch data from server end, it is better to fetch the data when it is needed rather than fetching all the data in the App component.
You need to do the onChange at the app level. You should just pass the onChange function into MetaFieldsInput and always use this.props.metafields when rendering

Close a dropdown when an element within it is clicked

I'm working on a Notification feature in my app (pretty much like Facebook notifications).
When I click a button in the header navigation, the dropdown opens and shows the notification list. The notification has a Link (from react-router) in it.
What I need to do is to close the dropdown whenever a Link is clicked.
Here's roughly the hierarchy I currently have:
Header > Navigation > Button > Dropdown > List > Notification > Link
Since the dropdown functionality is used more that once, I've abstracted its behavior away into a HOC that uses render prop:
export default function withDropDown(ClickableElement) {
return class ClickableDropdown extends PureComponent {
static propTypes = {
children: PropTypes.func.isRequired,
showOnInit: PropTypes.bool,
};
static defaultProps = {
showOnInit: false,
};
state = {
show: !!this.props.showOnInit,
};
domRef = createRef();
componentDidMount() {
document.addEventListener('mousedown', this.handleGlobalClick);
}
toggle = show => {
this.setState({ show });
};
handleClick = () => this.toggle(true);
handleGlobalClick = event => {
if (this.domRef.current && !this.domRef.current.contains(event.target)) {
this.toggle(false);
}
};
render() {
const { children, ...props } = this.props;
return (
<Fragment>
<ClickableElement {...props} onClick={this.handleClick} />
{this.state.show && children(this.domRef)}
</Fragment>
);
}
};
}
The HOC above encloses the Button component, so I have:
const ButtonWithDropdown = withDropdown(Button);
class NotificationsHeaderDropdown extends PureComponent {
static propTypes = {
data: PropTypes.arrayOf(notification),
load: PropTypes.func,
};
static defaultProps = {
data: [],
load: () => {},
};
componentDidMount() {
this.props.load();
}
renderDropdown = ref => (
<Dropdown ref={ref}>
{data.length > 0 && <List items={this.props.data} />}
{data.length === 0 && <EmptyList />}
</Dropdown>
);
render() {
return (
<ButtonWithDropdown count={this.props.data.length}>
{this.renderDropdown}
</ButtonWithDropdown>
);
}
}
List and Notification are both dumb functional components, so I'm not posting their code here. Dropdown is pretty much the same, with the difference it uses ref forwarding.
What I really need is to call that .toggle() method from ClickableDropdown created by the HOC to be called whenever I click on a Link on the list.
Is there any way of doing this without passing that .toggle() method down the Button > Dropdown > List > Notification > Link subtree?
I'm using redux, but I'm not sure this is the kind of thing I'd put on the store.
Or should I handle this imperatively using the DOM API, by changing the implementation of handleGlobalClick from ClickableDropdown?
Edit:
I'm trying with the imperative approach, so I've changed the handleGlobalClick method:
const DISMISS_KEY = 'dropdown';
function contains(current, element) {
if (!current) {
return false;
}
return current.contains(element);
}
function isDismisser(dismissKey, current, element) {
if (!element || !contains(current, element)) {
return false;
}
const shouldDismiss = element.dataset.dismiss === dismissKey;
return shouldDismiss || isDismisser(dismissKey, current, element.parentNode);
}
// Then...
handleGlobalClick = event => {
const containsEventTarget = contains(this.domRef.current, event.target);
const shouldDismiss = isDismisser(
DISMISS_KEY,
this.domRef.current,
event.target
);
if (!containsEventTarget || shouldDismiss) {
this.toggle(false);
}
return true;
};
Then I changed the Link to include a data-dismiss property:
<Link
to={url}
data-dismiss="dropdown"
>
...
</Link>
Now the dropdown is closed, but I'm not redirected to the provided url anymore.
I tried to defer the execution of this.toggle(false) using requestAnimationFrame and setTimeout, but it didn't work either.
Solution:
Based on the answer by #streletss bellow, I came up with the following solution:
In order to be as generic as possible, I created a shouldHideOnUpdate prop in the ClickableDropdown dropdown component, whose Hindley-Milner-ish signature is:
shouldHideOnUpdate :: Props curr, Props prev => (curr, prev) -> Boolean
Here's the componentDidUpdate implementation:
componentDidUpdate(prevProps) {
if (this.props.shouldHideOnUpdate(this.props, prevProps)) {
this.toggle(false);
}
}
This way, I didn't need to use the withRouter HOC directly in my withDropdown HOC.
So, I lifted the responsibility of defining the condition for hiding the dropdown to the caller, which is my case is the Navigation component, where I did something like this:
const container = compose(withRouter, withDropdown);
const ButtonWithDropdown = container(Button);
function routeStateHasChanged(currentProps, prevProps) {
return currentProps.location.state !== prevProps.location.state;
}
// ... then
render() {
<ButtonWithDropdown shouldHideOnUpdate={routeStateHasChanged}>
{this.renderDropdown}
</ButtonWithDropdown>
}
It seems you could simply make use of withRouter HOC and check if this.props.location.pathname has changed when componentDidUpdate:
export default function withDropDown(ClickableElement) {
class ClickableDropdown extends Component {
// ...
componentDidUpdate(prevProps) {
if (this.props.location.pathname !== prevProps.location.pathname) {
this.toggle(false);
}
}
// ...
};
return withRouter(ClickableDropdown)
}
Is there any way of doing this without passing that .toggle() method down the Button > Dropdown > List > Notification > Link subtree?
In the question, you mention that you are using redux.So I assume that you store showOnInit in redux.We don't usually store a function in redux.In toggle function,I think you should dispatch an CHANGE_SHOW action to change the showOnInit in redux, then pass the show data not the function to the children component.Then after reducer dispatch,the react will change “show” automatically.
switch (action.type) {
case CHANGE_SHOW:
return Object.assign({}, state, {
showOnInit: action.text
})
...
default:
return state
}
Link element and data pass
Use the property in Link-to,not data-...Like this:
<Link
to={{
pathname: url,
state:{dismiss:"dropdown"}
}}
/>
And the state property will be found in this.props.location.
give context a little try(not recommend)
It may lead your project to instable and some other problems.(https://reactjs.org/docs/context.html#classcontexttype)
First,define context
const MyContext = React.createContext(defaultValue);
Second,define pass value
<MyContext.Provider value={this.toggle}>
Then,get the value in the nested component
<div value={this.context} />

"Cannot update during an existing state transition" error in React

I'm trying to do Step 15 of this ReactJS tutorial: React.js Introduction For People Who Know Just Enough jQuery To Get By
The author recommends the following:
overflowAlert: function() {
if (this.remainingCharacters() < 0) {
return (
<div className="alert alert-warning">
<strong>Oops! Too Long:</strong>
</div>
);
} else {
return "";
}
},
render() {
...
{ this.overflowAlert() }
...
}
I tried doing the following (which looks alright to me):
// initialized "warnText" inside "getInitialState"
overflowAlert: function() {
if (this.remainingCharacters() < 0) {
this.setState({ warnText: "Oops! Too Long:" });
} else {
this.setState({ warnText: "" });
}
},
render() {
...
{ this.overflowAlert() }
<div>{this.state.warnText}</div>
...
}
And I received the following error in the console in Chrome Dev Tools:
Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be
a pure function of props and state; constructor side-effects are an
anti-pattern, but can be moved to componentWillMount.
Here's a JSbin demo. Why won't my solution work and what does this error mean?
Your solution does not work because it doesn't make sense logically. The error you receive may be a bit vague, so let me break it down. The first line states:
Cannot update during an existing state transition (such as within render or another component's constructor).
Whenever a React Component's state is updated, the component is rerendered to the DOM. In this case, there's an error because you are attempting to call overflowAlert inside render, which calls setState. That means you are attempting to update state in render which will in then call render and overflowAlert and update state and call render again, etc. leading to an infinite loop. The error is telling you that you are trying to update state as a consequence of updating state in the first place, leading to a loop. This is why this is not allowed.
Instead, take another approach and remember what you're trying to accomplish. Are you attempting to give a warning to the user when they input text? If that's the case, set overflowAlert as an event handler of an input. That way, state will be updated when an input event happens, and the component will be rerendered.
Make sure you are using proper expression. For example, using:
<View onPress={this.props.navigation.navigate('Page1')} />
is different with
<View onPress={ () => this.props.navigation.navigate('Page1')} />
or
<View onPress={ () => {
this.props.navigation.navigate('Page1')
}} />
The two last above are function expression, the first one is not. Make sure you are passing function object to function expression () => {}
Instead of doing any task related to component in render method do it after the update of component
In this case moving from Splash screen to another screen is done only after the componentDidMount method call.
import React, { Component } from 'react';
import {
StyleSheet,
Text,
View,
Button,
Image,
} from 'react-native';
let timeoutid;
export default class Splash extends Component {
static navigationOptions = {
navbarHidden: true,
tabBarHidden: true,
};
constructor(props) {
super(props)
this.state = { navigatenow: false };
}
componentDidMount() {
timeoutid=setTimeout(() => {
this.setState({ navigatenow: true });
}, 5000);
}
componentWillUnmount(){
clearTimeout(timeoutid);
}
componentDidUpdate(){
const { navigate,goBack } = this.props.navigation;
if (this.state.navigatenow == true) {
navigate('Main');
}
}
render() {
//instead of writing this code in render write this code in
componenetDidUdpate method
/* const { navigate,goBack } = this.props.navigation;
if (this.state.navigatenow == true) {
navigate('Main');
}*/
return (
<Image style={{
flex: 1, width: null,
height: null,
resizeMode: 'cover'
}} source={require('./login.png')}>
</Image>
);
}
}
Call the component props at each time as new render activity. Warning occurred while overflow the single render.
instead of
<Item onPress = { props.navigation.toggleDrawer() } />
try like
<Item onPress = {() => props.navigation.toggleDrawer() } />
You can also define the function overflowAlert: function() as a variable like so and it will not be called immediately in render
overflowAlert = ()=>{//.....//}

Categories

Resources