demian85
demian85

Reputation: 2494

How to simulate events using React-Redux?

I'm building a desktop app using React and Electron. Since it's growing fast, I realized I need some kind of state management like Redux to avoid passing many properties between components. I started reading Redux official documentation but still cannot figure out how to implement it in my case. I'm stuck!

For example, I have a main App component that renders many sub-components. One of them has a button. When clicked, it should dispatch an "event" to the store so the main App can act in consequence. How can I accomplish that? I cannot find the concept of events and I've hit a wall on how to even start using Redux.

Why events? Because it seems silly to me to dispatch an action and modify app state in this case. I just want to inform the root component to dispatch an action based on a user action. User interacts with a presentational component that should tell a container component to make an API call or start capturing audio/camera for example.

For what I know up to now, the only way to accomplish this is to mutate state so another component listening for changes detects a special value that means "hey, let's do this", then mutate state again to say "hey, I'm doing this", and when it's done state changes again with "hey, it's done".

Can someone point me in the right direction please?

Upvotes: 9

Views: 8631

Answers (3)

mglonnro
mglonnro

Reputation: 161

For what it's worth, I had a similar problem (click a button elsewhere in the tree and cause a map to reset its viewport) and solved it with a simple incremental key.

Button dispatches action:

export const RESET_MAP = "RESET_MAP";
export const resetMap = () => {
  return {
    type: RESET_MAP,
  };
};

In reducer:

case RESET_MAP:
  return Object.assign({}, state, {
    setvar: state.setvar + 1
  });

In map:

static getDerivedStateFromProps(newProps, state) {
  var newState = null;

  if (newProps.setvar !== state.setvar) {
      newState = {
        setvar: newProps.setvar,
        [other magic to reset the viewport]
      }
  }

  return newState;

Upvotes: 0

Richard Scarrott
Richard Scarrott

Reputation: 7063

Is the reason you think it seems silly because you don't want your presentational components to be redux-aware? If so mapDispatchToProps and bindActionCreators might help tidy things up, for example:

// App.js
import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { someAction } from './actions';
import Button from './Button';

const App = ({ onButtonClick }) => (
    <div>
        Hello.
        <Button onClick={onButtonClick}>Click me.</Button>
    </div>
);

export default connect(null, dispatch => {
    return bindActionCreators({
        onButtonClick: someAction
    }, dispatch);
})(App);

// Button.js
import React from 'react';

export default Button = ({ onClick, children }) => <button onClick={onClick}>{children}</button>;

As you can see only the connected container component is aware of the action, the Button (and even the App) are unaware that click triggers an action.

Upvotes: 0

azium
azium

Reputation: 20634

User interacts with a presentational component that should tell a container component to make an API call or start capturing audio/camera for example.

Perhaps your container component is doing more than it should. Consider a situation where React components do no more than two things:

  1. Display DOM elements based on props
  2. Handle user input (dispatch events)

If you were not using redux and wanted to make an API call when clicking a button, that might look something like:

class App extends Component {
  state = { data: {} }

  makeAPICall() {
    fetch(url).then(data => this.setState({ data }))
  }

  render() {
    <Child 
      data={this.state.data} 
      makeAPICall={this.makeAPICall} 
    />
  }
}

let Child = ({ data, makeAPICall }) => (
  <button onClick={makeAPICall}>Call API!</button>
)

The App component is responsible for storing global state and handling events, but we have to pass down that state and App's handlers through the component tree, quite possibly through components that will never themselves use those props.

By adding Redux your application now has a much better place to handle side effects like API calls or turning a camera on. Middleware!

Let this (crappy) illustration help you:

enter image description here

So now instead your App component can be just a normal presentational component like all of the others, simply displaying data based on store props and handling any user input / dispatching actions if need be. Let's update the above example using the thunk middleware

// actions.js
export let makeAPICall = () => {
  return dispatch => {
    fetch(url).then(data => dispatch({ 
      type: 'API_SUCCESS',
      payload: data,
    })).catch(error => dispatch({ type: 'API_FAIL', payload: error }))
  }
}

// Child.js
import { connect } from 'react-redux'
import { makeAPICall } from './actions'

let Child = ({ dispatch }) => (
  <button onClick={() => dispatch(makeAPICall())}>Call API!</button>
)

export default connect()(Child)

Thinking about React applications this way is very powerful. The separation of concerns is very well laid out. Components display stuff and handle events. Middleware takes care of any side effects (if there need to be any) and the store simply is an object that will cause React to re-render in case its data changes.

UPDATE: "The Modal Problem"

React apps may have some global stuff like modals and tooltips. Don't think about the "open modal" event.. think "what is the current modal content?".

A modal setup may look something along these lines:

// modalReducer.js
function reducer (state = null, action) {
   if (action.type === 'UPDATE_MODAL') {
     return action.payload
   } 

   // return default state
   return state
}

// App.js
let App = connect(state => ({ modal: state.modal }))(
  props => 
    <div>
       <OtherStuff />
       <Modal component={props.modal} />
    </div>
)

// Modal.js
let Modal = props =>
  <div
    style={{ 
      position: 'fixed',
      width: '100vw', height: '100vh',
      opacity: props.component ? 1 : 0,
    }}
  >
    {props.component}
  </div>

// Child.js

let Child = connect()(props =>
  <button onClick={e =>
    dispatch({ 
      type: 'UPDATE_MODAL'
      payload: <YourAwesomeModal />
    })
  }>
    Open your awesome modal!
  </button>
)

This is just an example, but would work great! when state.modal is null your Modal has 0 opacity and won't show. When you dispatch UPDATE_MODAL and pass in a component, the modal will show whatever you dispatch and change the opacity to 1 so you can see it. Later you can dispatch { type: 'UPDATE_MODAL', payload: null } to close the modal.

Hopefully this gives you some things to think about!

Definitely read this answer by Dan. His approach is similar but stored modal "metadata" vs the component itself which lends itself better to Redux fanciness like time travel etc.

Upvotes: 8

Related Questions