Skip to content

Proposal: Component level state reducer #275

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
adriancooney opened this issue Jan 28, 2016 · 13 comments
Closed

Proposal: Component level state reducer #275

adriancooney opened this issue Jan 28, 2016 · 13 comments

Comments

@adriancooney
Copy link

I understand component level state has be discussed quite in-depth here but I was wondering if this proposed API might be considered. It's probably a little too messy with the underlying implementation having to create unique ID's for each component and be the same ID every time (for time travel etc.) but worth the discussion I feel (and as the CONTRIBUTING.md says to discuss before wasting my time implementing).

What I'm proposing is adding another argument to the connect function that allows you to specify a reducer for the component's state. This state would then be accessed via a state prop (or another more appropriate name if that identifier is already taken).

Example:

import React, { Component } from "react";
import { connect } from "react-redux";

class Login extends Component {
    render() {
        const { state } = this.props;

        if(state.error) {
            // Error like "Invalid credentials"
            return <p>{ state.error.message }</p>
        }

        return //...form
    }
}

const componentReducer = (state = {}, action) => {
    switch action.type {
        case "USER_LOGIN_FAILURE": // Listen to any actions
            // Set the `error` key in the component's state
            return { error: action.error }

        default:
            return state;
    }
}

export default connect(mapStateToProps, actions, componentReducer)(Login)
Problems
  • ID's for components need to be generated in a deterministic way (hash the global state and component index?).
  • Dispatching actions that are intended for only one component would require some sort of instance of ID comparison. For example, you couldn't just dispatch a DROPDOWN_SELECT because all dropdowns mounted would then have the same selection.
@gaearon
Copy link
Contributor

gaearon commented Jan 28, 2016

I'd like to see a proof of concept for this.

@adriancooney
Copy link
Author

Sweet, I'll get on it.

@adriancooney
Copy link
Author

I've put together a quick and dirty implementation if you want to play around with it:

@tomkis
Copy link

tomkis commented Feb 3, 2016

This can certainly live in user land - example

@jgoux
Copy link

jgoux commented Feb 4, 2016

Hello,
I think this project could be a good starting point : https://github.com/tomchentw/redux-component
I'd also like this feature, so I don't need to use setState anymore. \o/

@slorber
Copy link
Contributor

slorber commented Feb 10, 2016

i'd also like it to be more easy to mount local state in redux store.

See also: https://github.com/threepointone/redux-react-local

@galkinrost
Copy link

In our applications we solved this problem in connect-like style. https://github.com/Babo-Ltd/redux-state

@jvanleeuwen
Copy link

@denis-sokolov
Copy link

We have solved this in redux-cursor with cursors slicing the state and providing local actions and reducers.

@timdorr timdorr closed this as completed Jul 10, 2016
@lucasconstantino
Copy link

lucasconstantino commented Mar 9, 2017

Came up with something easy here:

import { connect } from 'react-redux'
import { compose, withProps } from 'recompose'
import { createAction } from 'redux-actions'

export const WITH_REDUCER = 'withReducer/WITH_REDUCER'
export const withReducerAction = createAction(WITH_REDUCER)

export const withReducer = reducers => compose(
  connect(),
  withProps(props => Object.keys(reducers).reduce(
    (newProps, name) => ({
      ...newProps,
      [name]: payload => props.dispatch(withReducerAction({
        reducer: reducers[name],
        payload,
        props,
      }))
    }), {}
  ))
)

export default (state, { type, payload: { reducer, props, payload } }) =>
  type === WITH_REDUCER ? reducer(props)(state, { type, payload }) : state

Add the exported reducer to the store, and use the helper as such:

const Comp = ({ myComponentReducer, state }) => (
  <div>
    <p>{ state }</p>
    <input type="text" onChange={ (e) => myComponentReducer(e.value) } />
  </div>
)

const myComponentReducer = props => (state, { payload }) => payload

export default compose(
  connect(state => ({ state })),
  withReducer({ myComponentReducer })
)(Comp)

Quite nice... I'll make a module soon.

@Wroud
Copy link

Wroud commented Apr 2, 2018

Im implementing this in my project, example below from here.
Im use HOC to get ref to component, create root reducer in HOC, map component reducer to it and ref link to component instance.
Root reducer is like that:

rootReducers.forEach(reducer => {
        reducer.components.forEach(component => {
            component.setState((prevState, props) => reducer.reducer(props, prevState, action));
        });
    });

Layout.ts

export class LayoutClass extends React.Component {
    render() {
        return 
            (
                <NavMenu switchDrawer={this.props.actions.switchDrawer} key={"drawer"} />
            );
    }
}

const enhance = compose(
    withRouter,
    connect(
        null,
        lecturersActions.mapDispatch.ui,
    ),
);

export const Layout = enhance(LayoutClass);

DrawerWraper.ts

const initState = {
    open: true,
};

export class DrawerWraperClass extends React.Component {
    constructor(props) {
        super(props);
        this.state = initState;
    }
    closeDrawer = () => {
        this.setState({ open: false });
    }
    openDrawer = () => {
        this.setState({ open: true });
    }
    render() {
        return (
            <Drawer 
            persistent={true} 
            open={this.state.open} 
            onClose={this.closeDrawer} 
            onOpen={this.openDrawer}
            >
                ...
            </Drawer>
        );
    }
}

const switchDrawer = (props, prevState) => ({ open: !prevState.open });

const stateReducer = reducer => reducer
    .on(lecturersActions.actions.ui.switchDrawer, switchDrawer);

const enhance = compose(
    withRouter,
    connectState(
        initState,
        stateReducer,
        "drawer",
    ),
);

export const DrawerWrapper = enhance(DrawerWraperClass);

@lucasconstantino
Copy link

lucasconstantino commented Apr 2, 2018

Forgot to mention: I have indeed published a package for this: https://github.com/lucasconstantino/redux-transient

@Wroud perhaps you could give an opinion on that :)

@Wroud
Copy link

Wroud commented Apr 8, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests