React/Redux animations based on actions - javascript

Having more or less completed my first React+Redux application, I have come to the point where I would like to apply animations to various parts of the application. Looking at existing solutions, I find that there is nothing even close to what I would like. The whole ReactCSSTransitionGroup seems to be both an intrusive and naive way to handle animations. Animation concerns bleed out of the component you want to animate and you have no way to know anything about what happens in the application. From my initial analysis I have come up with the following requirements for what I would consider a good animation API:
The parent component should be ignorant of how the child component fades in/out (maybe with the exception of staggered animations).
The animations should integrate with React such that components are allowed to fade out (or otherwise complete their animations) before they are removed.
It should be possible to apply an animation to a component without modifying the component. It is ok that the component is styled such as to be compatible with the animation, but there should be no props, state, contexts or components related to the animation, nor should the animation dictate how the component is created.
It should be possible to perform an animation based on an action and the application state - in other words, when I have the full semantic context of what happened. For example, I might fade in a component when a thing has been created, but not when the page loads with the item in it. Alternatively, I might select the proper fade out animation, based on the user's settings.
It should be possible to either enqueue or combine an animation for a component.
It should be possible to enqueue an animation with a parent component. For example, if a component has two sub components and opening one would first trigger the other to close before opening itself.
The specifics of enqueuing animations can be handled by an existing animation library, but it should be possible to tie it in with the react and redux system.
One approach I have tried out is to create a decorator function like this (it is TypeScript, but I don't think that matters too much with regards to the problem):
export function slideDown<T>(Component: T) {
return class FadesUp extends React.Component<any, any> {
private options = { duration: 0.3 };
public componentWillEnter (callback) {
const el = findDOMNode(this).childNodes[0] as Element;
if (!el.classList.contains("animated-element")) {
el.classList.add("animated-element");
}
TweenLite.set(el, { y: -el.clientHeight });
TweenLite.to(el, this.options.duration, {y: 0, ease: Cubic.easeIn, onComplete: callback });
}
public componentWillLeave (callback) {
const el = findDOMNode(this).childNodes[0] as Element;
if (!el.classList.contains("animated-element")) {
el.classList.add("animated-element");
}
TweenLite.to(el, this.options.duration, {y: -el.clientHeight, ease: Cubic.easeIn, onComplete: callback});
}
public render () {
const Comp = Component as any;
return <div style={{ overflow: "hidden", padding: 5, paddingTop: 0}}><Comp ref="child" {...this.props} /></div>;
}
} as any;
}
...which can be applied like this...
#popIn
export class Term extends React.PureComponent<ITermStateProps & ITermDispatchProps, void> {
public render(): JSX.Element {
const { term, isSelected, onSelectTerm } = this.props;
return <ListItem rightIcon={<PendingReviewIndicator termId={term.id} />} style={isSelected ? { backgroundColor: "#ddd" } : {}} onClick={onSelectTerm}>{term.canonicalName}</ListItem>;
}
}
Unfortunately it requires the component to be defined as a class, but it does make it possible to declaratively add an animation to a component without modifying it. I like this approach but hate that I have to wrap the component in a transition group - nor does it address any of the other requirements.
I don't know enough about the internal and extension points of React and Redux to have a good idea how to approach this. I figured thunk actions would be a good place to manage the animation flows but I don't want to send the action components into the actions. Rather, I would like to be able to retrieve the source component for an action or something like that. Another angle could be a specialized reducer which passes in both the action and the source component, allowing you to match them somehow and schedule animations.
So I guess what I'm after is one or more of the following:
Ways to hook into React and/or Redux, preferably without destroying performance or violating the basic assumptions of the libraries.
Whether there are any existing libraries that solves some or all of these issues and which would be easy to integrate into the application.
Techniques or approaches to achieve all or most of these goals by either working with the normal animation tools or integrating well into the normal building blocks.

I hope I understood everything right… Dealing with React + Redux means, that in best case your components are pure functional. So a component that should be animated should (IMHO) at least take one parameter: p, which represents the state of the animation. p should be in the interval [0,1] and zero stands for the start, 1 for the end and everything in between for the current progress.
const Accordion = ({p}) => {
return (
…list of items, each getting p
);
}
So the question is, how to dispatch actions over time (what is an asynchronous thing), after the animation started, until the animation is over, after a certain event triggered that process.
Middleware comes in handy here, since it can »process« dispatched actions, transform them into another, or into multiple
//middleware/animatror.js
const animator = store => next => action => {
if (action.type === 'animator/start') {
//retrieve animation settings
const { duration, start, … } = action.payload;
animationEngine.add({
dispatch,
action: {
progress: () => { … },
start: () => { … },
end: () => { … }
}
})
} else {
return next(action);
}
}
export default animator;
Whereby the animationEngine is an Instance of AnimatoreEngine, an Object that listens to the window.requestAnimationFrame event and dispatches appropriate Actions. The creation of the middleware can be used the instantiate the animationEngine.
const createAnimationMiddleware = () => {
const animatoreEngine = new AnimatorEngine;
return const animator = store => next => action => { … }
}
export default createAnimationMiddleware;
//store.js
const animatorMiddleware = createAnimationMiddleware();
…
const store = createStore(
…,
applyMiddleware(animatorMiddleware, …)
)
The basic Idea is, to »swallow« actions of type animator/start, or something else, and transform them into a bunch of »subactions«, which are configured in the action.payload.
Within the middleware, you can access dispatch and the action, so you can dispatch other actions from there, and those can be called with a progress parameter as well.
The code showed here is far from complete, but tried to figure out the idea. I have build »remote« middleware, which handles all my request like that. If the actions type is get:/some/path, it basically triggers a start action, which is defined in the payload and so on. In the action it looks like so:
const modulesOnSuccessAction => data {
return {
type: 'modules/listing-success',
payload: { data }
}
}
const modulesgetListing = id => dispatch({
type: `get:/listing/${id}`,
payload: {
actions: {
start: () => {},
…
success: data => modulesOnSuccessAction(data)
}
}
});
export { getListing }
So I hope I could transport the Idea, even if the code is not ready for Copy/Paste.

Related

Set initial state for material ui dialog

I have a MaterialUI dialog that has a few text fields, drop downs, and other things on it. Some of these elements need to be set to some value every time the dialog opens or re-opens. Others elements cannot be loaded until certain conditions exist (for example, user data is loaded).
For the 'resetting', I'm using the onEnter function. But the onEnter function doesn't run until entering (duh!)... but the render function, itself, still does - meaning any logic or accessing javascript variables in the JSX will still occur. This leaves the 'onEnter' function ill-equipped to be the place I set up and initialize my dialog.
I also can't use the constructor for setting/resetting this initial state, as the data I need to construct the state might not be available at the time the constructor loads (upon app starting up). Now, I could super-complicate my JSX in my render function and make conditionals for every data point... but that's a lot of overhead for something that gets re-rendered every time the app changes anything. (the material UI dialogs appear run the entire render function even when the 'open' parameter is set to false).
What is the best way to deal with initializing values for a material ui dialog?
Here is a super-dumbed-down example (in real life, imagine getInitialState is a much more complex, slow, and potentially async/network, function) - let's pretend that the user object is not available at app inception and is actually some data pulled or entered long after the app has started. This code fails because "user" is undefined on the first render (which occurs BEFORE the onEnter runs).
constructor(props) {
super(props);
}
getInitialState = () => {
return {
user: {username: "John Doe"}
}
}
onEnter = () => {
this.setState(this.getInitialState())
}
render() {
const { dialogVisibility } = this.props;
return (
<Dialog open={dialogVisibility} onEnter={this.onEnter}>
<DialogTitle>
Hi, {this.state.user.username}
</DialogTitle>
</Dialog> );
}
My first instinct was to put in an "isInitialized" variable in state and only let the render return the Dialog if "isInitialized" is true, like so:
constructor(props) {
super(props);
this.state = {
isInitialized: false
};
}
getInitialState = () => {
return {
user: {username: "John Doe"}
}
}
onEnter = () => {
this.setState(this.getInitialState(),
() => this.setState({isInitialized:true})
);
}
render() {
const { dialogVisibility } = this.props;
if(!this.state.isInitialized) {
return null;
}
return (
<Dialog open={dialogVisibility} onEnter={this.onEnter}>
<DialogTitle>
Hi, {this.state.user.username}
</DialogTitle>
</Dialog> );
}
As I'm sure you are aware... this didn't work, as we never return the Dialog in order to fire the onEnter event that, in turn, fires the onEnter function and actually initializes the data. I tried changing the !this.state.inInitialized conditional to this:
if(!this.state.isInitialized) {
this.onEnter();
return null;
}
and that works... but it's gives me a run-time warning: Warning: Cannot update during an existing state transition (such as withinrender). Render methods should be a pure function of props and state.
That brought me to a lot of reading, specifically, this question: Calling setState in render is not avoidable which has really driven home that I shouldn't be just ignoring this warning. Further, this method results in all the logic contained in the return JSX to still occur... even when the dialog isn't "open". Add a bunch of complex dialogs and it kills performance.
Surely there is a 'correct' way to do this. Help? Thoughts?
What you need conceptually is that when you are freshly opening the dialog, you want to reset some items. So you want to be able to listen for when the value of open changes from false to true.
For hooks, the react guide provides an example for keeping the "old" value of a given item with a usePrevious hook. It is then simply a matter of using useEffect.
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function MyDialog({ dialogVisibility }) {
const prevVisibility = usePrevious(dialogVisibility);
useEffect(() => {
// If it is now open, but was previously not open
if (dialogVisibility && !prevVisibility) {
// Reset items here
}
}, [dialogVisibility, prevVisibility]);
return <Dialog open={dialogVisibility}></Dialog>;
}
The same thing can be achieved with classes if you use componentDidUpdate and the previousProps parameter it receives.
export class MyDialog extends Component {
public componentDidUpdate({ dialogVisibility : prevVisibility }) {
const { dialogVisibility } = this.props;
if (dialogVisibility && !prevVisibility) {
// Reset state here
}
}
public render() {
const { dialogVisibility } = this.props;
return <Dialog open={dialogVisibility}></Dialog>;
}
}
You should use componentDidUpdate()
This method is not called for the initial render
Use this as an opportunity to operate on the DOM when the component has been updated
If you need data preloaded before the dialog is opened, you can use componentDidMount():
is invoked immediately after a component is mounted (inserted into the tree)
if you need to load data from a remote endpoint, this is a good place to instantiate the network request
React guys added the useEffect hook exactly for cases like the one you are describing, but you would need to refactor to a functional component.
Source: https://reactjs.org/docs/hooks-effect.html
This can be solved by doing leaving the constructor, getInitialState, and onEnter functions as written and making the following addition of a ternary conditional in the render function :
render() {
const { dialogVisibility } = this.props;
return (
<Dialog open={dialogVisibility} onEnter={this.onEnter}>
{this.state.isInitialized && dialogVisibility ?
<DialogTitle>
Hi, {this.state.user.username}
</DialogTitle> : 'Dialog Not Initialized'}
</Dialog> );
)}
It actually allows the dialog to use it's "onEnter" appropriately, get the right transitions, and avoid running any extended complex logic in the JSX when rendering while not visible. It also doesn't require a refactor or added programming complexity.
...But, I admit, it feels super 'wrong'.

React functional component is taking snapshot of state at the time of registering handler on websocket

react functional component is taking snapshot of state at the time of subscription.
For ex. PFB code.
If i click setSocketHandler button and then press setWelcomeString button. Now if i receive message over socket when i log welcomestring it is empty.
But if i click setWelcomeString button and then click setSocketHandler button. Now if i receive message on socket Welcome is getting logged on console.
I have seen same behaviour in project so just created this simple app to prove.
If i use class component which is commented below.. everything works fine.
So my question is why react functional component is working on a state at the time of reg and not on actual state at the time message is received.
This is very weird. How to make it work in functional component correctly.
import React, {useEffect, useState} from 'react';
import logo from './logo.svg';
import './App.css';
const io = require('socket.io-client');
const socket = io.connect('http://localhost:3000/');
const App : React.FunctionComponent = () => {
const [welcomeString, setWelcomeString] = useState("");
const buttonCliecked = () => {
console.log("clocked button");
setWelcomeString("Welcome")
}
const onsockethandlerclicked = () => {
console.log("socket handler clicked");
socket.on('out', () => {
console.log("Recived message")
console.log(welcomeString);
});
}
return (
<div>
<header className="component-header">User Registration</header>
<label>{welcomeString}</label>
<button onClick={buttonCliecked}>setWelcomeString</button>
<button onClick={onsockethandlerclicked}>setSocketHandler</button>
</div>
);
}
/*class App extends React.Component {
constructor(props) {
super(props);
this.state = {
welcomeString:""
}
}
buttonCliecked = () => {
console.log("clocked button");
this.setState({ welcomeString:"Welcome"})
}
onsockethandlerclicked = () => {
console.log("socket handler clicked");
socket.on('out', () => {
console.log("Recived message")
console.log(this.state.welcomeString);
});
}
render() {
return (
<div>
<header className="component-header">User Registration</header>
<label>{this.state.welcomeString}</label>
<button onClick={this.buttonCliecked}>setwelcomestring</button>
<button onClick={this.onsockethandlerclicked}>setSocketHandler</button>
</div>
);
}
}*/
export default App;
For those of us coming from a Redux background, useReducer can seem deceptively complex and unnecessary. Between useState and context, it’s easy to fall into the trap of thinking that a reducer adds unnecessary complexity for the majority of simpler use cases; however, it turns out useReducer can greatly simplify state management. Let’s look at an example.
As with my other posts, this code is from my booklist project. The use case is that a screen allows users to scan in books. The ISBNs are recorded, and then sent to a rate-limited service that looks up the book info. Since the lookup service is rate limited, there’s no way to guarantee your books will get looked up anytime soon, so a web socket is set up; as updates come in, messages are sent down the ws, and handled in the ui. The ws’s api is dirt simple: the data packet has a _messageType property on it, with the rest of the object serving as the payload. Obviously a more serious project would design something sturdier.
With component classes, the code to set up the ws was straightforward: in componentDidMount the ws subscription was created, and in componentWillUnmount it was torn down. With this in mind, it’s easy to fall into the trap of attempting the following with hooks
const BookEntryList = props => {
const [pending, setPending] = useState(0);
const [booksJustSaved, setBooksJustSaved] = useState([]);
useEffect(() => {
const ws = new WebSocket(webSocketAddress("/bookEntryWS"));
ws.onmessage = ({ data }) => {
let packet = JSON.parse(data);
if (packet._messageType == "initial") {
setPending(packet.pending);
} else if (packet._messageType == "bookAdded") {
setPending(pending - 1 || 0);
setBooksJustSaved([packet, ...booksJustSaved]);
} else if (packet._messageType == "pendingBookAdded") {
setPending(+pending + 1 || 0);
} else if (packet._messageType == "bookLookupFailed") {
setPending(pending - 1 || 0);
setBooksJustSaved([
{
_id: "" + new Date(),
title: `Failed lookup for ${packet.isbn}`,
success: false
},
...booksJustSaved
]);
}
};
return () => {
try {
ws.close();
} catch (e) {}
};
}, []);
//...
};
We put the ws creation in a useEffect call with an empty dependency list, which means it’ll never re-fire, and we return a function to do the teardown. When the component first mounts, our ws is set up, and when the component unmounts, it’s torn down, just like we would with a class component.
The problem
This code fails horribly. We’re accessing state inside the useEffect closure, but not including that state in the dependency list. For example, inside of useEffect the value of pending will absolutely always be zero. Sure, we might call setPending inside the ws.onmessage handler, which will cause that state to update, and the component to re-render, but when it re-renders our useEffect will not re-fire (again, because of the empty dependency list)—as a result that closure will go on closing over the now-stale value for pending.
To be clear, using the Hooks linting rule, discussed below, would have caught this easily. More fundamentally, it’s essential to break with old habits from the class component days. Do not approach these dependency lists from a componentDidMount / componentDidUpdate / componentWillUnmount frame of mind. Just because the class component version of this would have set up the web socket once, in componentDidMount, does not mean you can do a direct translation into a useEffect call with an empty dependency list.
Don’t overthink, and don’t be clever: any value from your render function’s scope that’s used in the effect callback needs to be added to your dependency list: this includes props, state, etc. That said—
The solution
While we could add every piece of needed state to our useEffect dependency list, this would cause the web socket to be torn down, and re-created on every update. This would hardly be efficient, and might actually cause problems if the ws sends down a packet of initial state on creation, that might already have been accounted for, and updated in our ui.
If we look closer, however, we might notice something interesting. Every operation we’re performing is always in terms of prior state. We’re always saying something like “increment the number of pending books,” “add this book to the list of completed,” etc. This is precisely where a reducer shines; in fact, sending commands that project prior state to a new state is the whole purpose of a reducer.
Moving this entire state management to a reducer would eliminate any references to local state within the useEffect callback; let’s see how.
function scanReducer(state, [type, payload]) {
switch (type) {
case "initial":
return { ...state, pending: payload.pending };
case "pendingBookAdded":
return { ...state, pending: state.pending + 1 };
case "bookAdded":
return {
...state,
pending: state.pending - 1,
booksSaved: [payload, ...state.booksSaved]
};
case "bookLookupFailed":
return {
...state,
pending: state.pending - 1,
booksSaved: [
{
_id: "" + new Date(),
title: `Failed lookup for ${payload.isbn}`,
success: false
},
...state.booksSaved
]
};
}
return state;
}
const initialState = { pending: 0, booksSaved: [] };
const BookEntryList = props => {
const [state, dispatch] = useReducer(scanReducer, initialState);
useEffect(() => {
const ws = new WebSocket(webSocketAddress("/bookEntryWS"));
ws.onmessage = ({ data }) => {
let packet = JSON.parse(data);
dispatch([packet._messageType, packet]);
};
return () => {
try {
ws.close();
} catch (e) {}
};
}, []);
//...
};
While slightly more lines, we no longer have multiple update functions, our useEffect body is much more simple and readable, and we no longer have to worry about stale state being trapped in a closure: all of our updates happen via dispatches against our single reducer. This also aids in testability, since our reducer is incredibly easy to test; it’s just a vanilla JavaScript function. As Sunil Pai from the React team puts it, using a reducer helps separate reads, from writes. Our useEffect body now only worries about dispatching actions, which produce new state; before it was concerned with both reading existing state, and also writing new state.
You may have noticed actions being sent to the reducer as an array, with the type in the zero slot, rather than as an object with a type key. Either are allowed with useReducer; this is just a trick Dan Abramov showed me to reduce the boilerplate a bit :)
What about functional setState()
Lastly, some of you may be wondering why, in the original code, I didn’t just do this
setPending(pending => pending - 1 || 0);
rather than
setPending(pending - 1 || 0);
This would have removed the closure problem, and worked fine for this particular use case; however, the minute updates to booksJustSaved needed access to the value of pending, or vice versa, this solution would have broken down, leaving us right where we started. Moreover, I find the reducer version to be a bit cleaner, with the state management nicely separated in its own reducer function.
All in all, I think useReducer() is incredibly under-utilized at present. It’s nowhere near as scary as you might think. Give it a try!
Happy coding!

How do I use a withLatestFrom in an effect with selector with props (MemoizedSelectorWithProps) coming from the action

I have a selector with props (of type MemoizedSelectorWithProps). I'd like to use it in an effect inside WithLatestFrom. The thing is - the parameter for the selector (the props) is coming from the action payload. And I can't make the withLatestFrom access the action payload.
I'm using angular 7 (and naturally ngrx7). I've tried using a map to somehow create a new observable, but nothing is working...
these are some demo lines I've wrote here, to simplify my use case:
action:
export const GET_INVENTORY = '[App] Get Inventory';
export class GetInventory implements Action {
readonly type = GET_INVENTORY;
constructor (public branchId: number) {}
}
effect:
#Effect()
getInventory$ = this.actions$.pipe(
ofType(GET_INVENTORY)
withLatestFrom(this.store$.pipe(select(getIsStoreInventoryLoaded, {branchId: action.branchId}))), // this is not working obviously, since action is unknown
switchMap([action, loaded]: [GetInventory, boolean] => {
if (loaded) {
console.log('already loaded inventory for this branch', action.branchId);
} else {
console.log('never loaded inventory for this branch', action.branchId);
}
}
although this is a simplified version of my code, the design is kinda similar in my real project - I have a store with keys per "branch" inventory. say I'm a chain of supermarkets and each branch has it's own inventory page with lots of data and I want to not-fetch again if I already fetched. So if you have a different approach then working with MemoizedSelectorWithProps - feel free to suggest that too.
A simple switchMap or a mergeMap with a combineLatest should do the trick.
For example:
#Effect()
getInventory$ = this.actions$.pipe(
ofType(GET_INVENTORY),
mergeMap(action =>
combineLatest(
of(action),
this.store$.pipe(select(getIsStoreInventoryLoaded, {branchId: action.branchId}))
)
),
tap(([action, loaded]) => {
// The rest of your code...
})
)

React Redux - Trouble trying to use React-DnD and mapDispatchToProps

I was wondering why couldn't I get some of my components to work using ReactDnD and mapDispatchToProps.
I'm trying to drag and drop Services to Clients but I can't find my dispatch functions in props at my serviceSpec on the endDrag method.
Considering my mapDispatchToProps on my Service component:
const mapDispatchToProps = (dispatch) => ({
dragService: (service) => { dispatch(dragService(service)) },
dropService: (service, clientTarget) => { dispatch(dropService(service, clientTarget)) }
});
High-order functions to bond together DragSource + Service + State + Dispatch:
var reduxConnectedService = connect(mapStateToProps, mapDispatchToProps)(Service);
export default DragSource('service', serviceSpec, collect)(reduxConnectedService);
render() method:
render (){
const { isDragging, connectDragSource, service } = this.props;
return connectDragSource(
<a className="panel-block is-active">
<CategoryIcon category={service.category}/>
{service.name} | ${service.price}
</a>
)
}
The spec object used to implement the dragSource specification (here is the problem):
const serviceSpec = {
beginDrag(props) {
return props.service;
},
endDrag(props, monitor, component){
console.log(props);
}
}
The console.log at endDrag function just show my Service Object because is being returned on the beginDrag function:
{service: {…}}
But my plan was to dispatch the action dropService here on endDrag, but I couldn't. The documentation says that (http://react-dnd.github.io/react-dnd/docs/api/drag-source):
beginDrag(props, monitor, component): Required. When the dragging
starts, beginDrag is called. You must return a plain JavaScript object
describing the data being dragged. What you return is the only
information available to the drop targets about the drag source so
it's important to pick the minimal data they need to know. You may be
tempted to put a reference to the component into it, but you should
try very hard to avoid doing this because it couples the drag sources
and drop targets. It's a good idea to return something like { id:
props.id } from this method.
I don't believe that I should return the dropService(dispatch) function on the beginDrag definition. So after hours trying to make it work, I started to pass the dropService function as a prop directly through the parent component (ServiceList):
{this.props.filteredServices.map((service, index) => (
<Service service={service} key={service.name} dropService={this.props.dropService}/>
))}
Making this way I could dispatch the dropService action on the endDrag method like I wanted, the console.log can proves that:
{service: {…}, dropService: ƒ}
I could make it work but I can't understand why I couldn't get this to work using mapDispatchToProps. Is there any limitation while using React-DnD or am I making something wrong?
Any help will be appreciated, I cannot die with this doubt. Thank you in advance.
Your problem is with these two lines:
var reduxConnectedService = connect(mapStateToProps, mapDispatchToProps)(Service);
export default DragSource('service', serviceSpec, collect)(reduxConnectedService);
Note the order: you wrap Service into a Redux container. Then you wrap the Redux Container with the DragSource container. Thus, in the component tree, the drag container is the parent of the Redux container, which means it doesn't receive the Redux props from it.
To fix that, make the drag container the child of the Redux container. You can do so by simply swapping the DragSource() and connect() calls:
var dragService = DragSource('service', serviceSpec, collect)(Service);
var reduxConnectedService = connect(mapStateToProps, mapDispatchToProps)(dragService);

Flux / Fluxible: Updating routes based on state change

What is the most concise way to trigger route changes based on a change to a state store, using Fluxible and react router?
An example component might take some user input and call an Action on a click event (shortened for brevity)
class NameInput extends React.Component {
constructor (props) {
super(props);
this.state = props.state;
this.handleClick = this.handleClick.bind(this);
}
handleClick (event) {
this.context.executeAction(setName, {name:'Some User Value'});
}
render () {
return (
<div>
<input type="button" value="Set Name" onClick={this.handleClick} />
</div>
);
}
}
export default Home;
The handleClick method executes an Action which can update a Store with our new value.
But what if I also want this to trigger a navigation after the Store is updated? I could add the router context type and directly call the transition method after executing the Action:
this.context.executeAction(setName, {name:'Some User Value'});
this.context.router.transitionTo('some-route');
But this assumes that the setName Action is synchronous. Is this conceptually safe, on the assumption that the new route will re-render once the Action is completed and the Store is updated?
Alternatively, should the original Component listen for Store Changes and start the route transition based on some assessment of the store state?
Using the Fluxible, connectToStores implementation, I can listen for discreet changes to Store state:
NameInput = connectToStores(NameInput, [SomeStore], function (stores, props) {
return {
name: stores.SomeStore.getState().name
}
});
How a could a Store listener of this type be used to initiate a Route change?
I've noticed in my own application that for these kinds of flows it's usually safer to let actions do all the hard work. Annoying thing here is that the router isn't accessible from actions.
So, this is what I'd do:
Create a meta-action that does both: setNameAndNavigate. As payload you use something like this:
{
name: 'Value',
destination: {to: 'some-route', params: []},
router: this.context.router
}
Then, in your action, do the navigating when the setName completes.
It's not ideal, especially the passing of the Router to the action. There's probably some way to attach the Router to the action context, but that's not as simple as I had hoped. This is probably a good starting point though.
Extra reading:
Why do everything in actions? It's risky to execute actions in components in response to store changes. Since Fluxible 0.4 you can no longer let actions dispatch inside another dispatch. This happens a lot faster than you think, for example, executing an action in response to a change in a store, without delaying it with setImmediate or setTimeout, will kill your application since store changes happen synchronously during a dispatch.
Inside actions however, you can easily execute actions and dispatches, and wait for them to complete before executing the next one.
The end result of working this way is that most of your application logic has moved to actions, and your components turn into simple views that set-and-forget actions only in response to user interactions (clicks/scrolling/hover/..., as long as it's not in response to a store change).
The Best way is to create a new action as #ambroos suggested,
setNameAndNavigate. For navigation though, use the navigateAction
https://github.com/yahoo/fluxible/blob/master/packages/fluxible-router/lib/navigateAction.js, you would only have to give the url as argument.
Something like this,
import async from 'async';
import setName from 'some/path/setName';
export default function setNameAndNavigate(context, params, done) {
async.waterfall([
callback => {
setName(context, params, callback);
},
(callback) => {
navigate(context, {
url: '/someNewUrl',
}, callback);
}
], done);
}
let your actions be the main workers as much as possible.

Categories