AdamBrodzinski / meteor-flux-leaderboard

Flux Example with React & Meteor
131 stars 19 forks source link

Desperately waiting Redux example. #4

Closed SenerDemiral closed 9 years ago

AdamBrodzinski commented 9 years ago

Hoping to get one done this week. Still not 100% sure how the async works.

AdamBrodzinski commented 9 years ago

@SenerDemiral Here's a simple example test. I don't know how bad it is to update and fetch data from Minimongo right in the reducer. Seems ok to me but i'm sure there's a better way.

_.extend is being used here to make a copy of the state and merge the new data together (not mutating old state).

let { createStore, combineReducers } = Redux;

let initialState = {
  selectedId: '',
  selectedPlayerName: ''
}

function playerUI(state = initialState, action) {
  debugger

  switch (action.type) {
    case 'CHANGE_NAME':
      debugger
      return _.extend({}, state, {
        selectedPlayerName: action.selectedName
      });
    case 'SELECT_PLAYER':
      return _.extend({}, state, {
        selectedId: action.selectedId
      });
    default:
      return state;
  }
}

function players(state = [], action) {
  switch (action.type) {

    case 'INCREMENT_SCORE':
      Players.update({_id: action.playerId}, {$inc: {score: 5}});
      return _.extend({}, state, {
        players: Players.find().fetch()
      });
    default:
      return state;
  }
}

let app = combineReducers({
  playerUI,
  players,
});

store = createStore(playerUI);

// store.getState()
// {"selectedId": "", "selectedPlayerName": "" }

// store.dispatch({type: "CHANGE_NAME", selectedName: 'Adam'});

// store.getState()
// {"selectedId": "", "selectedPlayerName": "Adam" }
SenerDemiral commented 9 years ago

Thank you, I will try.

wuxianliang commented 9 years ago

Hi @SenerDemiral @AdamBrodzinski Is this project helpful? https://github.com/zhongqf/meteor-react-redux-example I wish more documents and more clear answer to whether we should use react.js with Npm and Webpack or with Meteor Packages as much as possible.

AdamBrodzinski commented 9 years ago

@wuxianliang ah thanks! I'll check it out. I noticed it's using webpack... you might be interested in this project (here's a fork that will be merged soon) https://github.com/AdamBrodzinski/meteor-webpack-react/tree/minimal

It also adds server support so you can use webpack full stack.

AdamBrodzinski commented 9 years ago

I also came up with an altered test that moves the mutation to the action creator (top func)

store.dispatch(incScore('bkQdAN7Simdys7YCZ'))

(though I haven't checked out the above link though so this may/prob. will change)

let { createStore, combineReducers } = Redux;

// action creator
incScore = function incScore(playerId) {
  Players.update({_id: playerId}, {$inc: {score: 5}});
  let allPlayers = Players.find().fetch();
  return { type: 'INCREMENT_SCORE', players: allPlayers };
}

function players(state = [], action) {
  switch (action.type) {
    case 'INCREMENT_SCORE':
      return action.players;
    default:
      return state;
  }
}

let initialState = {
  selectedId: '',
  selectedPlayerName: ''
}

function playerUI(state = initialState, action) {
  switch (action.type) {
    case 'CHANGE_NAME':
      return _.extend({}, state, {
        selectedPlayerName: action.selectedName
      });
    case 'SELECT_PLAYER':
      return _.extend({}, state, {
        selectedId: action.selectedId
      });
    default:
      return state;
  }
}

let app = combineReducers({
  playerUI,
  players,
});

store = createStore(app);
rolfnl commented 9 years ago

I also have it in the actions, reducers just create a new state. But, I'll ask around, I had the same question anyway. I remember 'reducers' from some other languages/projects and they had db calls in it, but I think with Redux/Flux I've seen more examples where the meat of the things like async/promises calls are in action creators. Or, I think via middleware they are actually different kind of actions (like you dispatch an action or a promise). Anyway, in the functions that we use in actions ;) you'd talk to the db.

I've also thought about abstracting the Meteor/Mongo code out of the actions and import them, so there's no direct relation between them. But, sometimes I go overboard with abstracting and it's not nice anymore. I mean, the reason I started with Meteor in the first place was because I could do many things in a simple way without worrying about OOP things (doing a lot in PHP/Laravel/Symfony) ;)

AdamBrodzinski commented 9 years ago

:+1: Cool thanks!

One thing I should mention... my snippet above will actually make the UI render twice, once when doing the update and again when the (missing) flux collection helper would fire a 'changed' action on the players collection.

In hindsight it should prob. look like this (assuming the meteor flux helper)

// action creator
incScore = function incScore(playerId) {
  Players.update({_id: playerId}, {$inc: {score: 5}}, (err) => {
    if (err) store.dispatch({type: "INCREMENT_FAILED"})
  });

  return { type: 'INCREMENT_SCORE', (not sure what payload would be??) };
}

Then on change the collection would fire the change to make the store update.

rolfnl commented 9 years ago

Yes that's better. Because you want to dispatch an action for the possible changes. And then the reducer for that action changes the state object.

In this case the INCREMENT_SCORE action could fetch all players and send them down, right?

payload could be original playerId and score value? I think you're free to choose whatever the payload is, right? I mean, if it helps the next action then send it along.

AdamBrodzinski commented 9 years ago

In this case the INCREMENT_SCORE action could fetch all players and send them down, right?

So currently this example publishes all the players so there's no need to subscribe, if that's what you mean by 'send them down'. Once the update is called, the minimongo collection will instantly change regardless of the server response, so the store would get the optimistic update. I'll have a better example of the flux-helper with this soon.

payload could be original playerId and score value? I think you're free to choose whatever the payload is, right? I mean, if it helps the next action then send it along.

Yep I agree. I guess you don't even need a payload, as long as an action is sent and then the store could just return the state param (because it will update collection state on the next tick anyway)

rolfnl commented 9 years ago

[...] Once the update is called, the minimongo collection will instantly change regardless of the server response, so the store would get the optimistic update.

Yes, I agree.

[...] I guess you don't even need a payload, as long as an action is sent and then the store could just return the state param (because it will update collection state on the next tick anyway)

Agree, if a payload doesn't make sense (I also try to avoid the payload term, at least, it's too much Flux instead of Redux, haha, anyway, usually I just return the incoming vars (for favourite console.log/debugger statements) or nothing.

AdamBrodzinski commented 9 years ago

@SenerDemiral this new article may help too, just came out yesterday: http://rackt.github.io/redux/docs/advanced/AsyncActions.html

Hoping to finish up the Redux branch today :+1:

AdamBrodzinski commented 9 years ago

@SenerDemiral Hot off the press! https://github.com/AdamBrodzinski/meteor-flux-leaderboard/tree/redux

Please let me know how it can be improved and what parts are confusing. I also made nice step by step commits to build it up from the beginning (as well as migrate away from Alt).

I'm really liking Redux compared to all of the others!

wuxianliang commented 9 years ago

That is Great! Thank you very very much for doing this project. @AdamBrodzinski I have been running the codes. It works fine. But one hint, usually leaderboard has the feature of ordering by score for showing reactivity. one more enhancement that I do like it very much is taking react-motion in. See this demo. https://cdn.rawgit.com/chenglou/react-motion/e8f42dcd9678a8cea8648a3cf4f994583a99e7f7/demos/demo8/index.html

If we break the order by drag-drop, then the demo correct us elegantly. That is a concept proof.

AdamBrodzinski commented 9 years ago

That is Great! Thank you very very much for doing this project.

No prob! It was actually really fun! Now the bad part is i'm thinking of migrating a React Native app and Meteor app to Redux.... there goes my weekend :laughing:

But one hint, usually leaderboard has the feature of ordering by score for showing reactivity.

Oh good point! I think I just used Players fetch without the sort. I'll get that squared away tonight. Currently trying to get the dev-tools to work without hot-reloader (may have to resort to using meteor-webpack... which is actually super nice)

one more enhancement that I do like it very much is taking react-motion in. See this demo. If we break the order by drag-drop, then the demo correct us elegantly. That is a concept proof.

Oh man!!! Wow, thanks!! I forgot about that React Europe video too. I have a trello like project coming up in the next few months and this would work great (well maybe a bit less floaty lol). I'll def. add this just so I can try it out :+1: :beers:


Another thing I need to add is error handling... which should dispatch an action in Players.update callback. Loading should also have a state now that I think of it. So loading is true on start then every dispatch would set it to false

AdamBrodzinski commented 9 years ago

@wuxianliang the sorting is fixed. You may want to re-checkout the action and reducer now. The action is currently fetching right after the update and returns the data as a payload. The reducer takes the array and does a clone then sort before returning.

I have to add this to the docs but its super important to not mutate the action or cause side effects in the reducer or you will lose replay

SenerDemiral commented 9 years ago

Thank you for your amazing effort. I am too new to Web concept. I satarted to learn Web and Meteor 4 mounths ago after 30 years desktop programming with SQL and plus inadequate english, so it takes some time to assimilate. (Everythings change too quickly for me: Meteor, Blaze, React, Redux, ....)

SenerDemiral commented 9 years ago

If I want to insert new name and also select this new item, what is the correct position to use

store.dispatch(Actions.selectPlayer(playerId));

OK?

Actions.insertName = function insertName(name) {
  let playerId = Players.insert({name: name, score: 10});
  // TODO call FAILED action on error
  store.dispatch(Actions.selectPlayer(playerId));
  return { type: 'INSERT_NAME' };
};
AdamBrodzinski commented 9 years ago

Yea I think that's ok! The main thing is that you don't want to dispatch a new action in the reducer. I might also call it addPlayer instead of insertName but that's not a big deal.

One things that is different with Meteor is we can do a synchronous DB insert that would normally be async for other frameworks. Here's an issue on the subject: https://github.com/rackt/redux/issues/199

SenerDemiral commented 9 years ago

Thanks, Despite knowing so little to this subject, I think Redux has brilliant future in Flux world.

rolfnl commented 9 years ago

Hey @AdamBrodzinski, maybe you have an idea...

I don't like it that store is a global in your example. I know Meteor is all about globals and I don't mind them in Meteor land, but for some reason when I'm using something else that is not 100% from Meteor I try to avoid them. So this is also the case when working with React, React Redux, etc. (For some reason I don't mind using Meteor globals inside React, e.g. in a data wrapper component that subscribes to a collection inside componentWillMount...)

Also, since we're binding store like <Provider store={ store }>...</Provider> it feels kind of wrong that's also available as global, you know what I mean?

As for actions I try to send them down as props e.g. using bindActionCreators that allows many options how to implement/use them, but my preference is that I have e.g. this.props.actions.selectPlayer available in the component instead of using the global Actions.selectPlayer (making the component less dumb).

OK, so, nothing new for you there, and this is a matter of preference imho... the Redux "pattern" doesn't enforce this at all.

However, do you have any idea how to make store not global but still work with flux-helpers because in the trackCollection method you actually want to dispatch an action. And right now that works because store is global :) I can't wrap my head around some solution while using flux-helper/trackCollection (or something else just as neat).

I've also toyed with wrapper/data components, as talked about here and here, but to me it's not as clean. Perhaps with React 0.14 where we can use higher-order functions instead for those containers instead of components.

AdamBrodzinski commented 9 years ago

Great stuff @rolfnl ! I haven't played with bindActionCreators yet but that looks interesting. My personal preference is to make the components as dumb as possible but having it as a prop isn't too bad. I did this at first in React Native and it was a pain to thread the props down more than 2 levels (and confusing as to where the source originated at).

Here are some ideas to make store not global. If you can define everything in the same file for store you could just expose the dispatcher like this:

dispatcher = store.dispatcher;

This is slightly better since you can't get to the store and getState. Being that the React binding handle this it shouldn't be too big of a deal. The flux helper can still use dispatcher as needed.

My preference so far though is to use meteor-webpack and use real modules. Then the dispatcher and store won't be global (also a security risk IMHO).

BTW, have you tried Redux with react router yet? I have yet to try it out.

wuxianliang commented 9 years ago

Dear @AdamBrodzinski I am trying to add "react-dnd" --drag drop to redux branch. I tried very hard on this, but stuck by one problem -- "connectDragSource is not a function". There are many examples with pure React project, no Meteor integrated one. So may I ask for a help, you can do your own one or just point out where I am wrong. I think there is a basic wrong understanding. Thank you very much.

wuxianliang commented 9 years ago

for packages.json

{
  "externalify": "0.1.0",
  "redux": "1.0.1",
  "react-redux": "0.8.2",
  "react-dnd":"1.1.4"
}

at browserfy

Redux = require("redux");
ReactRedux = require("react-redux");
Provider = ReactRedux.Provider;
//connect = ReactRedux.connect;
DragDropContext = require('react-dnd').DragDropContext;
HTML5Backend = require('react-dnd/modules/backends/HTML5');
DragSource = require('react-dnd').DragSource;
DropTarget = require('react-dnd').DropTarget;

at the root component

let AppContainer = React.createClass({
  componentWillMount() {
    this.sub = Meteor.subscribe('players');
  },

  componentWillUnmount() {
    this.sub.stop();
  },

  render() {
    //debugger // checkout this.props with debugger!
    return (<App {...this.props} />);
  }
});
DragDropContext(HTML5Backend)(AppContainer);
function mapStateToProps(state) {
  return {
    players: state.players,
    selectedId: state.userInterface.selectedId,
    selectedPlayerName: state.userInterface.selectedPlayerName
  };
}

this.AppContainer = ReactRedux.connect(mapStateToProps)(AppContainer);

at the drag item

var itemSource = {
  beginDrag: function (props) {
    return {id:props.player._id};
  }
};

function collect(connect, monitor) {
  return {
    connectDragSource: connect.dragSource(),
    isDragging: monitor.isDragging()
  }
}

PlayerItem = React.createClass({
  handleClick() {
    var playerId = this.props.player._id;
    store.dispatch(Actions.selectPlayer(playerId));
  },

  getClassName() {
    var selectedId = this.props.selectedPlayerId;
    var playerId = this.props.player._id;
    return (selectedId === playerId) ? 'player selected' : 'player';
  },

  render() {
    var connectDragSource = this.props.connectDragSource;
    var isDragging = this.props.isDragging;
    var player = this.props.player;
    return connectDragSource(
      <li className={ this.getClassName() } onClick={ this.handleClick }>
        <span className="name">{ player.name }</span>
        <span className="score">{ player.score }</span>
      </li>
    );
  }
});
DragSource('item', itemSource, collect)(PlayerItem);
wuxianliang commented 9 years ago

With so many examples and good tutorial, I think it is one hour work. It took me four without success. I will sleep one hour and back. I am coding a schedule arrangement for my working school. I have to make it work and today.

AdamBrodzinski commented 9 years ago

Hmmm, not sure offhand. I've used http://rubaxa.github.io/Sortable/ for drag and drop before... if that would work for you use case it's pretty easy to use (and has a React mixin)

wuxianliang commented 9 years ago

I have figure out the problem. There should be

PlayerItem = DragSource('item', itemSource, collect)(PlayerItem);
wuxianliang commented 9 years ago

Dear @AdamBrodzinski Based on the Redux branch of this projects I code a course schedule app. I have used it to schedule courses of our grade this semester. Hope to learn more. Thank you very much for sharing this project.

https://paike.meteor.com https://github.com/wuxianliang/Course-Scheduling-By-Drag-Drop

AdamBrodzinski commented 9 years ago

Very cool @wuxianliang ! Looks really nice :smile: :beers:

AdamBrodzinski commented 9 years ago

closing this out... feel free to open up any new issues/discussions