Enzyme testing with React/Redux - shallow rendering issues with setState - reactjs

I have a React component that loads another component if it has it's initial local state altered. I can't get a clean test going because I need to set local state AND shallow render so the child component doesn't crash when the new component mounts because the redux store isn't there. It seems those two objectives are incompatible in Enzyme.
For the child component to display, things need to occur:
The component needs to receive a "response" props (any string will do)
The component need to have it's initial "started" local state updated to true. This is done with a button in the actual component.
This is creating some headaches with testing. Here is the actual line that determines what will be rendered:
let correctAnswer = this.props.response ? <div className="global-center"><h4 >{this.props.response}</h4><Score /></div> : <p className="quiz-p"><strong>QUESTION:</strong> {this.props.currentQuestion}</p>;
Here is my current Enzyme test:
it('displays score if response and usingQuiz prop give proper input', () => {
const wrapper = shallow(<Quiz usingQuiz={true} answers={[]} response={'example'}/>);
wrapper.setState({ started: true })
expect(wrapper.contains(<Score />)).toEqual(true)
});
I am using shallow, because any time I use mount, I get this:
Invariant Violation: Could not find "store" in either the context or props of "Connect(Score)". Either wrap the root component in a <Provider>, or explicitly pass "store" as a prop to "Connect(Score)".
Because the component is displayed through the parent, I can not simply select the disconnected version. Using shallow seems to correct this issue, but then I can not update the local state. When I tried this:
it('displays score if response and usingQuiz prop give proper input', () => {
const wrapper = shallow(<Quiz usingQuiz={true} answers={[]} response={'example'}/>);
wrapper.setState({ started: true })
expect(wrapper.contains(<Score />)).toEqual(true)
});
The test fails because shallow doesn't let the DOM get updated.
I need BOTH conditions to be met. I can do each condition individually, but when I need to BOTH 1) render a component inside a component (needs shallow or will freak about the store not being there), and 2) update local state (needs mount, not shallow), I can't get everything to work at once.
I've looked at chats about this topic, and it seems this is a legitimate limitation of Enzyme, at least in 2017. Has this issue been fixed? It's very difficult to test this.
Here is the full component if anyone needs it for reference:
import React from 'react';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { Transition } from 'react-transition-group';
import { answerQuiz, deleteSession, getNewQuestion } from '../../actions/quiz';
import Score from '../Score/Score';
import './Quiz.css';
export class Quiz extends React.Component {
constructor(props) {
super(props);
// local state for local component changes
this.state = {
started: false
}
}
handleStart() {
this.setState({started: true})
}
handleClose() {
this.props.dispatch(deleteSession(this.props.sessionId))
}
handleSubmit(event) {
event.preventDefault();
if (this.props.correctAnswer && this.props.continue) {
this.props.dispatch(getNewQuestion(this.props.title, this.props.sessionId));
}
else if (this.props.continue) {
const { answer } = this.form;
this.props.dispatch(answerQuiz(this.props.title, answer.value, this.props.sessionId));
}
else {
this.props.dispatch(deleteSession(this.props.sessionId))
}
}
render() {
// Transition styles
const duration = 300;
const defaultStyle = {
opacity: 0,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
height: '100%',
width: '100%',
margin: '0px',
zIndex: 20,
top: '0px',
bottom: '0px',
left: '0px',
right: '0px',
position: 'fixed',
display: 'flex',
alignItems: 'center',
transition: `opacity ${duration}ms ease-in-out`
}
const transitionStyles = {
entering: { opacity: 0 },
entered: { opacity: 1 }
}
// Response text colors
const responseClasses = [];
if (this.props.response) {
if (this.props.response.includes("You're right!")) {
responseClasses.push('quiz-right-response')
}
else {
responseClasses.push('quiz-wrong-response');
}
}
// Answer radio buttons
let answers = this.props.answers.map((answer, idx) => (
<div key={idx} className="quiz-question">
<input type="radio" name="answer" value={answer} /> <span className="quiz-question-label">{answer}</span>
</div>
));
// Question or answer
let correctAnswer = this.props.response ? <div className="global-center"><h4 className={responseClasses.join(' ')}>{this.props.response}</h4><Score /></div>: <p className="quiz-p"><strong>QUESTION:</strong> {this.props.currentQuestion}</p>;
// Submit or next
let button = this.props.correctAnswer ? <button className="quiz-button-submit">Next</button> : <button className="quiz-button-submit">Submit</button>;
if(!this.props.continue) {
button = <button className="quiz-button-submit">End</button>
}
// content - is quiz started?
let content;
if(this.state.started) {
content = <div>
<h2 className="quiz-title">{this.props.title} Quiz</h2>
{ correctAnswer }
<form className="quiz-form" onSubmit={e => this.handleSubmit(e)} ref={form => this.form = form}>
{ answers }
{ button }
</form>
</div>
} else {
content = <div>
<h2 className="quiz-title">{this.props.title} Quiz</h2>
<p className="quiz-p">So you think you know about {this.props.title}? This quiz contains {this.props.quizLength} questions that will test your knowledge.<br /><br />
Good luck!</p>
<button className="quiz-button-start" onClick={() => this.handleStart()}>Start</button>
</div>
}
// Is quiz activated?
if (this.props.usingQuiz) {
return (
<Transition in={true} timeout={duration} appear={true}>
{(state) => (
<div style={{
...defaultStyle,
...transitionStyles[state]
}}>
{/* <div className="quiz-backdrop"> */}
<div className="quiz-main">
<div className="quiz-close" onClick={() => this.handleClose()}>
<i className="fas fa-times quiz-close-icon"></i>
</div>
{ content }
</div>
</div>
)}
</Transition >
)
}
else {
return <Redirect to="/" />;
}
}
}
const mapStateToProps = state => ({
usingQuiz: state.currentQuestion,
answers: state.answers,
currentQuestion: state.currentQuestion,
title: state.currentQuiz,
sessionId: state.sessionId,
correctAnswer: state.correctAnswer,
response: state.response,
continue: state.continue,
quizLength: state.quizLength,
score: state.score,
currentIndex: state.currentIndex
});
export default connect(mapStateToProps)(Quiz);
Here is my test using mount (this crashes due to a lack of store):
import React from 'react';
import { Quiz } from '../components/Quiz/Quiz';
import { Score } from '../components/Score/Score';
import { shallow, mount } from 'enzyme';
it('displays score if response and usingQuiz prop give proper input', () => {
const wrapper = mount(<Quiz usingQuiz={true} answers={[]} response={'example'}/>);
wrapper.setState({ started: true })
expect(wrapper.contains(<Score />)).toEqual(true)
});
});

This looks like a component that should be tested with mount(..).
How are you importing your connected component Score and Quiz?
I see that you are already correctly exporting Quiz component and default exporting the connected Quiz component.
Try importing with
import { Score } from '../Score/Score';
import { Quiz } from '../Quiz/Quiz';
in your test, and mount(..)ing. If you are importing from default export, you will get a connected component imported, which I think is the cause of the error.

Are you sure that Transition component let it's content to be displayed? I use this component and can't properly handle it in tests...
Can you for the test purposes alter your renders return with something like this:
if (this.props.usingQuiz) {
return (
<div>
{
this.state.started && this.props.response ?
(<Score />) :
(<p>No score</p>)
}
</div>
)
}
And your test can look something like this:
it('displays score if response and usingQuiz prop give proper input',() => {
const wrapper = shallow(<Quiz usingQuiz={true} answers={[]} response={'example'}/>);
expect(wrapper.find('p').text()).toBe('No score');
wrapper.setState({ started: true });
expect(wrapper.contains(<Score />)).toEqual(true);
});
I also tested shallows setState and little test like this works fine:
test('HeaderComponent properly opens login popup', () => {
const wrapper = shallow(<HeaderComponent />);
expect(wrapper.find('.search-btn').text()).toBe('');
wrapper.setState({ activeSearchModal: true });
expect(wrapper.find('.search-btn').text()).toBe('Cancel');
});
So I believe that shallow properly handle setState and the problem caused by some components inside your render.

The reason you are getting that error is because you're trying to test the wrapper component generated by calling connect()(). That wrapper component expects to have access to a Redux store. Normally that store is available as context.store, because at the top of your component hierarchy you'd have a <Provider store={myStore} />. However, you're rendering your connected component by itself, with no store, so it's throwing an error.
Also, if you are trying to test a component inside a component, may full DOM renderer may be the solution.
If you need to force the component to update, Enzyme has your back. It offers update() and if you call update() on a reference to a component that will force the component to re-render itself.

Related

Dynamically creating a new component based off another component's state

First timer here on StackOverflow! I'm trying to emulate a terminal interface for one of my portfolio projects. The way I have envisioned the terminal is that each terminal box has a state object with a few key/value pairs. Ideally, when someone enters text into the terminal box input form, the input becomes disabled and a new terminal box is rendered on the screen with a dynamic response based upon the userInput text which has been saved in the state. Where I'm stuck:
Once userInput state has been updated, how do I get a new terminal box to render beneath the prior box on the screen?
Prior to rendering, how do I set the initial state of the newly-rendered terminal box back to default with the exception of the "output" which would be re-valued to an appropriate response that I set?
How do I access the state in the prior terminal box so I can "read" the userInput stored there so I can determine what the appropriate response to that input would be?
I've included copies of each of the components below:
App.js
import React from "react";
import Terminal from "./components/Terminal";
import "./App.css";
class App extends React.Component {
render() {
return (
<div>
<Terminal />
</div>
);
}
}
export default App;
Terminal.js
import React, { Component } from "react";
import Form from "./Form";
import Falcon from "./Falcon";
import Messages from "./Alerts/Messages";
class Terminal extends Component {
state = {
output: Messages.intro,
userInput: "",
isComplete: false,
isDisabled: "",
};
markComplete = () => {
this.setState({
isComplete: true,
});
};
onSubmit = (event, userInput) => {
event.preventDefault();
this.setState({
userInput: userInput,
isDisabled: "disabled",
});
};
render() {
return (
<div>
<Falcon
output={this.state.output}
markComplete={this.markComplete}
isComplete={this.state.isComplete}
/>
<p />
<Form
input={this.state.userInput}
onSubmit={this.onSubmit}
isComplete={this.state.isComplete}
isDisabled={this.state.isDisabled}
/>
<p />
</div>
);
}
}
export default Terminal;
Falcon.js (Note: You'll see that there is a component "Typed" below - that is part of Matt Boldt's Typed.js (of which react-typed is an offshoot package) package which I'm using to simulate typing.)
import React, { Component } from 'react'
import Typed from 'react-typed'
class Falcon extends Component {
state = {
output: this.props.output,
};
render() {
return (
<div>
<Typed
strings={[this.state.output]}
typeSpeed={40}
onComplete={(self) => {
self.cursor.remove();
this.props.markComplete();
}}
/>
</div>
);
}
}
export default Falcon;
Form.js
import React from "react";
class Form extends React.Component {
state = {
input: this.props.input,
};
render() {
return (
<form
style={{
display: this.props.isComplete === false ? "none" : "",
}}
onSubmit={(event) => {
this.props.onSubmit(event, this.state.input);
}}
>
{"> "}
<input
ref={(input) => input && input.focus()}
type="text"
disabled={this.props.isDisabled}
style={{
border: "none",
outline: "none",
backgroundColor: "#FFF",
color: "#000",
}}
value={this.state.input}
onChange={(event) => this.setState({ input: event.target.value })}
/>
</form>
);
}
}
export default Form;
Any insight or guidance you can offer would be much appreciated! Thank you for helping this "first-timer" out!
Welcome to StackOverflow! I made a codesandbox demo with a few changes.
When developing React applications, it's a good practice to model the UI (the DOM elements) as a function of your internal state. You update the state and the UI changes automatically, it reacts to updates.
That said, you probably want to consider using the form only for the actual input element at the bottom of the terminal. The "past buffer" is just an array that only increases its content with user input and program output. Another good practice (actually a commandment!) is to never mutate the state. So, if you want to update the array, you create a new one from scratch, as in:
this.setState((state) => ({
conversation: [
...state.conversation, // we spread the previous state into the new one
{ text: state.userInput, id: faker.random.uuid(), type: "input" } // the last element is appended
]
}));}
Notice how setState (in class components) just schedules an update for the fields that you used. As your app scales, you will probably want to limit the length of this array.
The terminal component could be like:
class Terminal extends Component {
state = {
output: "Messages.intro",
userInput: "",
isComplete: false,
isDisabled: "",
conversation: [] // couldn't think of a nice name :(
};
markComplete = () => {
this.setState({
isComplete: true
});
};
onChange = (event) => {
this.setState({ userInput: event.target.value });
};
onSubmit = (event) => {
event.preventDefault();
this.setState((state) => ({
userInput: "",
conversation: [
...state.conversation,
{ text: state.userInput, id: faker.random.uuid(), type: "input" }
]
}));
};
render() {
const { conversation, userInput, output, isComplete } = this.state;
return (
<div>
<Falcon
output={output}
markComplete={this.markComplete}
isComplete={isComplete}
/>
<p />
// This is not really a form. Should be changed to another readonly element
{conversation.map((item, index) => (
<Form
key={item.id}
input={item.text}
onSubmit={this.onSubmit}
isComplete={isComplete}
isDisabled
/>
))}
<p />
<Form
input={userInput}
onSubmit={this.onSubmit}
isComplete={isComplete}
isDisabled={false}
onChange={this.onChange}
/>
<p />
</div>
);
}
}

How do i load all images before rendering the page?

So i have a preloader that works, i just have some problems with my image component itself.
My image component below in my theory should after the first render get all of my image tags used in my project.
Then when the image event onLoad is fired, it should update a state which is didLoad and set it to true. When the page is rerendered again because of the updated state, it should check if didLoad is true, if it's true it should execute the useEffect inside the condition once and add 1 to the amount loaded.
And then i check if images is higher than 0, and images is equal to images loaded then remove preloader.
Obviously something is wrong since i can't get it to work, it needs to work with all images not one at a time because it's not a lazyload for the image itself, but the preloader for the whole page.
Ignore preload: false it's for when the preloader is removed, then it should play page animations.
/** #jsx jsx */
import React, { useContext, useEffect, useState } from "react";
import { css, jsx } from "#emotion/core";
import preloadContext from "../../context/preload.context";
const Img = ({ src, alt }) => {
const [didLoad, setLoad] = useState(false);
const [preload, setPreload] = useContext(preloadContext);
const imgStyle = css`
width: 100%;
height: 100%;
display: block;
object-fit: cover;
`;
useEffect(() => {
setPreload({
amount: preload.amount++,
preload: preload.preload,
isLoaded: preload.isLoaded,
});
}, []);
if (didLoad === true) {
useEffect(() => {
setPreload({
amount: preload.amount,
preload: preload.preload,
isLoaded: preload.isLoaded++,
});
}, []);
}
return (
<img
src={src}
onLoad={(e) => {
setLoad(true);
}}
css={imgStyle}
alt={alt}
onDragStart={(e) => e.preventDefault()}
/>
);
};
export default Img;
The answer is You can and You cannot.
We can : if you're trying to hold rendering of a certain component until an <img /> (Outside of this component is loaded)
We cannot : If you're trying to track loading of <img /> which is INSIDE of a not rendered component. In this case Image won't load until we render it's parent component.
For the first option here's how you can do it:
constructor(props) {
super(props);
this.state = { loaded: 0 };
this.handleImageLoaded = this.handleImageLoaded.bind(this);
this.image = React.createRef();
}
componentDidMount() {
const img = this.image.current;
if (img && img.complete) {
this.handleImageLoaded(10); // 100 / Number of images to track
}
}
handleImageLoaded(loadIncrement) {
if (!this.state.loaded)
this.setState({ loaded: loadIncrement });
}
render() {
return (
<div>
<img src='image.jpg' ref={this.image} onLoad={this.handleImageLoaded} />
{this.state.loaded >= 100 && <MyComponent />}
</div>
);
}
As mentioned in the comment you must divide 100 to number of images you wish to track and then one the image is loaded it will add the loading percentage to the parent state. One the loaded state reach 100 or above it will render <MyComponent />

ReactJS Accessing Props of Rendered Component

I am building a component that will be used for step-through processes such as :
This Workflow component takes an array of 'steps' as a prop and then it does the rest. Here is how it is being called in the image above :
let steps = [
{
display: "Sign Up Form",
component: SignupForm
},
{
display: "Verify Phone",
component: VerifyPhone
},
{
display: "Use Case Survey",
component: UseCase
},
{
display: "User Profile",
component: UserProfile
},
];
return (
<Workflow
steps={steps}
/>
);
The component field points to the component to be rendered in that step. For example the SignupForm component looks like this :
export default class SignupForm extends React.Component {
...
render() {
return (
<div>
<div className="page-header">
<h1>New User Sign Up Form</h1>
<p>Something here...</p>
</div>
<div className="form-group">
<input type="email" className="form-control" placeholder="Email address..." />
<small id="emailHelp" className="form-text text-muted">We'll never share your email with anyone else.</small>
</div>
</div>
);
}
}
The issue I'm facing is that in each step there needs to be a Next button to validate the information in that step and move to the next. I was going to just put that button inside the component of each step, but that makes it less user-friendly. When a user clicks 'Next', and everything is valid, that step should be collapsed and the next step should open up. However this means that my Workflow component needs to render this button.
So, I need my Workflow component to call the method of each step component to validate the information in the step and return a promise letting it know if it passed or failed (with any error message). How do I need to call this method? Here is where the Workflow component renders all the steps
as <step.component {...this.props} />:
{
this.state.steps.map((step, key) => {
return (
...
<Collapse isOpen={!step.collapsed}>
<step.component {...this.props} />
<Button color="primary"
onClick={() => this.validate(key)}>Next</Button>
<div className="invalid-value">
{step.error}
</div>
</Collapse>
...
);
})
}
That renders the next button, as well as the onClick handler validate():
validate(i) {
let steps = _.cloneDeep(this.state.steps);
let step = steps[i];
step.component.handleNext().then(function () {
...
}).catch((err) => {
...
});
}
Ideally, step.component.validate() would call the validate method inside that component that has already been rendered:
export default class SignupForm extends React.Component {
....
validate() {
return new Promise((resolve, reject) => {
resolve();
})
}
render() {
...
}
}
.. which would have access to the state of that component. But, thats not how it works. How can I get this to work? I read a little about forwarding refs, but not exactly sure how that works. Any help is greatly appreciated!
One approach is to apply the Observer pattern by making your form a Context Provider and making it provide a "register" function for registering Consumers. Your consumers would be each of the XXXForm components. They would all implement the same validate API, so the wrapping form could assume it could call validate on any of its registered components.
It could look something like the following:
const WorkflowContext = React.createContext({
deregisterForm() {},
registerForm() {},
});
export default class Workflow extends React.Component {
constructor(props) {
super(props);
this.state = {
forms: [],
};
}
deregisterForm = (form) => {
this.setState({
forms: this.state.forms.slice().splice(
this.state.forms.indexOf(forms), 1)
});
};
registerForm = (form) => {
this.setState({ forms: [ ...this.state.forms, form ] })
};
validate = () => {
const validationPromises = this.state.forms.reduce(
(promises, formComponent) => [...promises, formComponent.validate()]);
Promise.all(validationPromises)
.then(() => {
// All validation Promises resolved, now do some work.
})
.catch(() => {
// Some validation Promises rejected. Handle error.
});
};
render() {
return (
<WorkflowContext.Provider
value={{
deregisterForm: this.deregisterForm,
registerForm: this.registerForm,
}}>
{/* Render all of the steps like in your pasted code */}
<button onClick={this.validate}>Next</button
</WorkflowContext.Provider>
);
}
}
// Higher-order component for giving access to the Workflow's context
export function withWorkflow(Component) {
return function ManagedForm(props) {
return (
<WorkflowContext.Consumer>
{options =>
<Component
{...props}
deregisterForm={options.deregisterForm}
registerForm={options.registerForm}
/>
}
</WorkflowContext.Consumer>
);
}
}
SignupForm and any other form that needs to implement validation:
import { withWorkflow } from './Workflow';
class SignupForm extends React.Component {
componentDidMount() {
this.props.registerForm(this);
}
componentWillUnmount() {
this.props.deregisterForm(this);
}
validate() {
return new Promise((resolve, reject) => {
resolve();
})
}
render() {
...
}
}
// Register each of your forms with the Workflow by using the
// higher-order component created above.
export default withWorkflow(SignupForm);
This pattern I originally found applied to React when reading react-form's source, and it works nicely.

Rendering in react with array.map

I have an array of strings which I would like to render as a list, with a colored text. The user can change the color with a button.
For that I have built a component called which receives an array and renders a list with the array's values and a button to change the color:
import React, { Component } from "react";
const renderArray = arr => (arr.map(value => (
<li>
{value}
</li>
)))
class List extends Component {
constructor(props) {
super(props);
this.state = {
color: 'red'
}
}
toggleColor = () => {
if (this.state.color === "red") {
this.setState({color: "blue"});
} else {
this.setState({color: "red"});
}
}
render() {
const style = {
color: this.state.color
};
return (
<div style={style}>
<ul>
{renderArray(this.props.array)}
</ul>
<button onClick={this.toggleColor}>Change color</button>
</div>
);
}
}
export default List;
The List is called with:
<List array={arr} />
And arr:
const arr = ['one', 'two', 'three'];
Fiddle here: Fiddle
But this seems incorrect to me. I rerender the whole array by calling renderArray() each time the color changes. In this case it is not too bad but what if the renderArray() is much more complex?
To my understanding, I need to create a new list only if the array prop changes and this could do in getDerivedStateFromProps (or in componentWillReceiveProps which will be deprecated...):
componentWillReceiveProps(nextProps)
{
const renderedArray = renderArray(nextProps.array);
this.setState({ renderedArray });
}
And then, on render, use this.state.renderedArray to show the list.
But this seems strange, to store a rendered object in the state...
Any suggestions?
Thanks!
1) React uses the concept of virtual DOM to calculate the actual difference in memory and only if it exists, render the difference into DOM
2) You can "help" React by providing a "key", so react will better understand if it's needed to re-render list/item or not
3) Your code componentWillReceiveProps can be considered as a bad practice because you're trying to make a premature optimization. Is repaint slow? Did you measure it?
4) IMHO: renderArray method doesn't make sense and can be inlined into List component
React render the DOM elements efficiently by using a virtual DOM and checks if the update needs to happen or not and hence, it may not be an issue even if you render the list using props. To optimise on it, what you can do is to make use of PureComponent which does a shallow comparison of state and props and doesn't cause a re-render if nothing has changed
import Reactfrom "react";
const renderArray = arr => (arr.map(value => (
<li>
{value}
</li>
)))
class List extends React.PureComponent { // PureComponent
constructor(props) {
super(props);
this.state = {
color: 'red'
}
}
toggleColor = () => {
if (this.state.color === "red") {
this.setState({color: "blue"});
} else {
this.setState({color: "red"});
}
}
render() {
const style = {
color: this.state.color
};
return (
<div style={style}>
<ul>
{renderArray(this.props.array)}
</ul>
<button onClick={this.toggleColor}>Change color</button>
</div>
);
}
}
export default List;

React router native animation blinks before animation starts

I'm developing a react native app and using React router native v4, and I'm trying to develop the animation part, as suggested by documentation, first I made sure that everything works without animations or transitions.
I've iterated the implementation and this is as far as I got by now:
my main component renders the following:
// app.js:render
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
my routes.js renders the following (note the location prop passed to Switch to prevent it updating its children before the parent component):
// routes.js render
<ViewTransition pathname={location.pathname}>
<Switch location={location}>
<Route exact path={uri.views.main} component={Dashboard} />
<Route path={uri.views.login} component={Login} />
<Route path={uri.views.register} component={Register} />
</Switch>
</ViewTransition>
and the ViewTransition that handles the animation, by now it just fadesin/out the old and the new views:
// view-transition.js
#withRouter
export default class ViewTransition extends Component {
static propTypes = {
children: PropTypes.node,
location: PropTypes.object.isRequired,
};
state = {
prevChildren: null,
opacity: new Animated.Value(1)
};
componentWillUpdate(nextProps) {
if (nextProps.location !== this.props.location) {
this.setState({ prevChildren: this.props.children }, this.animateFadeIn);
}
}
animateFadeIn = () => {
Animated.timing(this.state.opacity, {
toValue: 0,
duration: 150
}).start(this.animateFadeOut);
};
animateFadeOut = () => {
this.setState({ prevChildren: null }, () => {
Animated.timing(this.state.opacity, {
toValue: 1,
duration: 400
}).start();
});
};
render() {
const { children } = this.props;
const { prevChildren, opacity } = this.state;
return (
<Animated.View
style={{
...StyleSheet.absoluteFillObject,
opacity,
position: "absolute"
}}
>
{prevChildren || children}
</Animated.View>
);
}
}
The code above is working, I can see old view fading out and new view fading in, but I have an issue, when it starts fading out, somehow the component remounts again and I can see a blink just before the animation starts, I wish to know what's wrong with my code.
I could fix my code above, it happened that the method componentWillUpdate in the lifecycle of a react component, already had passed the nextProps to the children, and in the meantime my component sets the new state with old children, the Switch is preparing the render of the new ones, that produces an unmount of the oldChildren and the mount of the new children, when finally my component finishes to set the state, the old children already had been unmounted and they have to be mounted again.
The story above is the long story of "I can see a blink when my animation starts", the solution happened to be easy, I don't check stuff in componentWillUpdate anymore but in componentWillReceiveProps, since the new props will pass to the parent component before its children, it gives me enough time to catch the current children, assign them to the state, render before Switch unmounts them and keep them in the View for the fading out, so no blinking anymore.
My final view-transition.js:
// view-transition.js
export default class ViewTransition extends Component {
static propTypes = {
children: PropTypes.node,
location: PropTypes.object.isRequired,
};
state = {
prevChildren: null,
opacity: new Animated.Value(1)
};
componentWillReceiveProps(nextProps) {
if (nextProps.location !== this.props.location) {
this.setState({ prevChildren: this.props.children }, this.animateFadeIn);
}
}
animateFadeIn = () => {
Animated.timing(this.state.opacity, {
toValue: 0,
duration: 150
}).start(this.animateFadeOut);
};
animateFadeOut = () => {
this.setState({ prevChildren: null }, () => {
Animated.timing(this.state.opacity, {
toValue: 1,
duration: 400
}).start();
});
};
render() {
const { children } = this.props;
const { prevChildren, opacity } = this.state;
return (
<Animated.View
style={{
...StyleSheet.absoluteFillObject,
opacity,
position: "absolute"
}}
>
{prevChildren || children}
</Animated.View>
);
}
}
I wrote an AnimatedSwitch component to work in React Native.
There is a brief moment where the states are updating and it would flash, but I fixed that with this code:
// Need to render the previous route for the time between not animating and animating
if (needsAnimation && !animating) {
const tempPreviousRoute = getPreviousRoute(
exact,
previousLocation,
previousChildren,
);
if (tempPreviousRoute) {
const prevRouteComp = renderPreviousRoute(tempPreviousRoute);
return prevRouteComp;
} else {
return null;
}
}
Complete code is here.
https://javascriptrambling.blogspot.com/2020/07/react-router-native-animatedswitch.html
Hmm..i know it's little bit too late, maybe you can try react-router-native-animate-stack
It is subcomponent of react-router-native. But it is animatable and provide stack feature. It is inspired by switch component, if you know switch, you will know how to use this package.
Default behaviour
Customization

Resources