morh
morh

Reputation: 101

Is there a valid way to use redux with react without react-redux?

I'm trying to learn redux and implementing it in react without react-redux. Why you ask? just want to learn vanilla redux. so I use this approach:

const render = () => ReactDOM.render(<App />, document.getElementById("root"));

store.subscribe(render);

render();

Now the redux store listens for a dispatch and runs the render callback on every single change. The problem is that all the components in the app will be rendered when a change to the store occurs,because we subscribe the root element. I want only the the relevant components(the components that actually use the state in the store that changed) to be rendered. Is there a way to do it only with "redux" without "react-redux"?

thanks.

Upvotes: 3

Views: 1437

Answers (2)

HMR
HMR

Reputation: 39270

Here is a very naive way to implement react-redux yourself that may give you a better understanding what goes on but I'd advice not to do this in any real application you'd write, only for learning purposes:

const { createStore, applyMiddleware, compose } = Redux;

const initialState = { counter: 0 };
//action types
const ADD = 'ADD';
//action creators
const add = (howMuch) => ({
  type: ADD,
  payload: howMuch,
});
const reducer = (state, { type, payload }) => {
  if (type === ADD) {
    return { ...state, counter: state.counter + payload };
  }
  return state;
};
//selectors
const selectCounter = (state) => state.counter;
//creating store with redux dev tools
//assuming only one store is used, useSelector and useDispatch
//  are based on this store
const composeEnhancers =
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
  reducer,
  initialState,
  composeEnhancers(
    applyMiddleware(() => (next) => (action) =>
      next(action)
    )
  )
);
//useDispatch will just return store.dispatch function
const useDispatch = () => store.dispatch;
//custom hook checking if component is still mounted
//  you should not set state if component is unmounted
const useIsMounted = () => {
  const isMounted = React.useRef(false);
  React.useEffect(() => {
    isMounted.current = true;
    return () => (isMounted.current = false);
  }, []);
  return isMounted;
};
//default compare function
const refCompare = (a, b) => a === b;
//useSelector will listen to store changes and set local state
//  if it changed
const useSelector = (selectFn, compareFn = refCompare) => {
  //set local state with the result from the selector
  const [state, setState] = React.useState(() =>
    selectFn(store.getState())
  );
  //to prevent setting state on unmounted component
  const mounted = useIsMounted();
  //effect to listen to store changes
  React.useEffect(() => {
    //listen to store changes and unsubscribe when unmount
    //  or functions change
    const unsubscribe = store.subscribe(() => {
      //run the selector
      const currentStoreState = selectFn(store.getState());
      //do not set state when component is unmounted
      if (!mounted.current) return;
      //call setState with callback when returning a different
      //  value the component should re render although it also
      //  re render sometimes when same value is returned
      //  I am not sure why this is but happens when pressing
      //  add count and then unrelated action
      //  bug reported here:
      //  https://github.com/facebook/react/issues/20817
      setState(
        (currentLocalState) =>
          //see if result of the selector changed
          compareFn(currentLocalState, currentStoreState)
            ? currentLocalState //do nothing, state didn't change
            : currentStoreState //state did change, assign it to local state
      );
    });
    //unsubscribe when unmounted or functions passed changed
    return unsubscribe;
  }, [compareFn, mounted, selectFn]);
  return state;
};
//Counter will re render when selectCounter(reduxState)
//  changes but will also sometimes render when it doesn't
//  not sure why but should not according to documentation
//  of setState
const Counter = React.memo(function Counter() {
  const counter = useSelector(selectCounter);
  console.log('rendering counter with', counter);
  return <div>{counter}</div>;
});
//should render only when state changes from odd to even
const OddEven = React.memo(function OddEven() {
  //should only render when isOdd changes but will render
  //  one extra time after a change and first time it
  //  doesn't change, not according to how setState should
  //  work and would be grateful if someone can explain
  //  why this happens as it's not according to setState
  //  documentation
  const isOdd = useSelector((state) =>
    Boolean(selectCounter(state) % 2)
  );
  console.log('rendering OddEven', isOdd);
  return <div>{isOdd ? 'odd' : 'even'}</div>;
});
//in this implementation no provider is needed because it doesn't
//  use React.context
const App = () => {
  const dispatch = useDispatch();
  console.log('in App render');
  return (
    <div>
      <button
        onClick={() =>
          dispatch({ type: 'unrelated action' })
        }
      >
        dispatch unrelated action (no re renders)
      </button>
      <button onClick={() => dispatch(add(1))}>
        add 1
      </button>
      <button onClick={() => dispatch(add(2))}>
        add 2
      </button>
      <Counter />
      <OddEven />
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<div id="root"></div>

Here is your render example, it doesn't work at all with pure components because it'll only render <App /> and all components in App are pure components meaning they would only re render if props passed to it would change.

const { createStore, applyMiddleware, compose } = Redux;

const initialState = { counter: 0 };
//action types
const ADD = 'ADD';
//action creators
const add = (howMuch) => ({
  type: ADD,
  payload: howMuch,
});
const reducer = (state, { type, payload }) => {
  console.log('action:',type);
  if (type === ADD) {
    return { ...state, counter: state.counter + payload };
  }
  return state;
};
//selectors
const selectCounter = (state) => state.counter;
//creating store with redux dev tools
//assuming only one store is used, useSelector and useDispatch
//  are based on this store
const composeEnhancers =
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
  reducer,
  initialState,
  composeEnhancers(
    applyMiddleware(() => (next) => (action) =>
      next(action)
    )
  )
);
const Counter = React.memo(function Counter() {
  const counter = selectCounter(store.getState());
  console.log('rendering counter with', counter);
  return <div>{counter}</div>;
});
const OddEven = React.memo(function OddEven() {
  const isOdd = Boolean(
    selectCounter(store.getState()) % 2
  );
  console.log('rendering OddEven', isOdd);
  return <div>{isOdd ? 'odd' : 'even'}</div>;
});
const App = () => {
  console.log('in app render');
  return (
    <div>
      <button
        onClick={() =>
          store.dispatch({ type: 'unrelated action' })
        }
      >
        dispatch unrelated action (no re renders)
      </button>
      <button onClick={() => store.dispatch(add(1))}>
        add 1
      </button>
      <button onClick={() => store.dispatch(add(2))}>
        add 2
      </button>
      <Counter />
      <OddEven />
    </div>
  );
};

const render = () =>
  ReactDOM.render(<App />, document.getElementById('root'));
store.subscribe(render);
render();
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<div id="root"></div>

Upvotes: 4

Andrea Costanzo
Andrea Costanzo

Reputation: 2215

  1. Yes, you can use redux without react-redux, the only thing you should keep in mind is that you have to do some additional activities to achieve the same result. Here I give you a detailed example: https://www.stuffthatstough.com/archive/Redux%20React%20without%20React%20Redux

  2. React redux is a UI binding library. This means that it contains all the helper methods that allow you actions like making it possible to update your UI and dispatch methods with ease. For example, react-redux's connect method allows mapping your store data/your actions to react props, making it easier to bind your UI components to redux. Without this connection, you would have to subscribe to a particular value in the store with a listener, and then calling a setState in the listener to react to the changes. When the number of listeners you need in a specific component increases, the code can become messy. In react-redux, however, by mapping state to props, all this data comes from props automatically, making it easier for you to manage them

Here you can find additional info: https://react-redux.js.org/introduction/why-use-react-redux

Upvotes: 0

Related Questions