ReactJS - app functions in different files - reactjs.net

I'm trying to make React-based web game. I have an App component which holds pretty much all non-UX state. To avoid code duplication I also hold most functions in it and pass it down as prop to child components.
But now I'm starting to get cluttered by different functions, all in the App body. Is there any simple way to satisfactory structure this in different files? Should I already look into state management libraries?
Currently stuff looks like:
class App extends Component {
constructor(props) {
super(props);
this.state = gameInitialize();
this.modifyState = this.modifyState.bind(this);
this.moveUnit = this.moveUnit.bind(this);
this.progressMission = this.progressMission.bind(this);
this.timeJump = this.timeJump.bind(this);
this.competenceAfterTimeJump = this.competenceAfterTimeJump.bind(this);
this.save = this.save.bind(this);
this.load = this.load.bind(this);
}
componentDidMount() {
this.timerID = setInterval(this.modifyState, this.state.interval);
window.addEventListener('beforeunload', this.save);
this.load();
}
componentWillUnmount() {
clearInterval(this.timerID);
}
save() {
localStorage.setItem("gameSave", toJson(this.state));
}
load() {
let state = 0;
try {
state = fromJson(localStorage.getItem("gameSave"));
} catch (error) {
console.log(error);
return 0;
}
state.units.map(unit => {
delete unit.__parent;
delete unit.attributes.__parent
return 0;
});
state.missions.map(mission => delete mission.__parent);
this.setState(state);
}
modifyState() {
this.setState(this.state.units.map(this.progressMission));
this.setState(this.state);
}
progressMission(unit) {
const mission = unit.currentMission;
let increment = unit.attributes[mission.type].total() - mission.complexity;
if (increment < 0) increment = 0;
mission.progress += increment * this.state.interval / 1000 * unit.competence / 10;
if (mission.progress >= mission.difficulty) {
mission.progress = 0;
this.state.experience.get(mission.reward);
mission.completions += 1;
}
}
moveUnit(unit, mission) {
unit.currentMission = mission;
this.setState(this.state);
}
timeJump() {
const game = this.state;
while (game.units.length > 2) {
game.units.pop();
};
game.units.map(function (unit) {
Object.keys(unit.attributes).map((key) => { unit.attributes[key] = newAttribute() });
unit.currentMission = game.missions[0];
});
game.missions.map((mission) => {mission.progress = 0});
game.units[0].competence = this.competenceAfterTimeJump();
game.experience.current = 0;
this.setState(game);
}
competenceAfterTimeJump() {
return (10 + Math.sqrt(this.state.experience.total) / 10);
}
render() {
return (
<div className="App">
<header className="App-header">
<h1 className="title">Time-traveling Hero: eventually I'll save the world, or maybe not if I don't feel it</h1>
</header>
<SaveLoad game={this} />
<Prestige game={this} />
<MissionWrapper>
<MissionList missions={this.state.missions} game={this} />
</MissionWrapper>
<UnitWrapper>
<ExpWrapper>
<div>
Available Experience: {this.state.experience.current.toFixed(1)}
</div>
<div>
Total Experience: {this.state.experience.total.toFixed(1)}
</div>
</ExpWrapper>
<UnitList units={this.state.units} game={this} />
</UnitWrapper>
</div>
);
}
}
function gameInitialize() {
let game = { units: [], missions: [], currentUnit: undefined };
game.interval = 10;
game.missions = generateMissions(50);
game.experience = {
current: 0, total: 0,
get: function (amount) { this.current += amount; this.total += amount },
spend: function (amount) {
if (this.current >= amount) {
this.current -= amount;
return true;
}
else return false;
}
};
game.units.push({ name: "Hero", attributes: newAttributes(), competence: 10, currentMission: game.missions[0] });
game.units.push({ name: "Childhood Friend", attributes: newAttributes(), competence: 15, currentMission: game.missions[0] });
game.currentUnit = game.units[0];
game.missionsWithUnits = function () {
this.missions.map()
}
return game;
}
How should I proceed?

Yes, it's super easy to organize JS code! Use modules. Here's how to do it.
Export functions from a file
adders.js:
export function addTwo (number) {
return number + 2
}
Then use it:
This could be in a component file:
import { addTwo } from './path/to/adders.js'
console.log(addTwo(5)) // logs 7
You can organize this super well for a lot of things. If you have a group of related functions, use a module like this. here's the file structure:
mathStuff/
adders.js
index.js
You have all of your related files in the same folder and your functions exported from the individual files like above. Then set up index like this:
index.js:
import * as adders from './adders.js'
// Set up your object however you want.
const MathStuff = {
...adders
}
export default MathStuff
Then in any component you can do this:
import MathStuff from './path/to/mathStuff'
MathStuff.addTwo(7) // 9
For even more organization, you could set your index up to have functions like this:
index.js:
import * as adders from './adders.js'
import * as dividers from './dividers.js' // another math file with division functions or something
// Set up your object however you want.
const MathStuff = {
adders,
dividers
}
export default MathStuff
And use it like this:
import MathStuff from './path/to/mathStuff' // points to directory, NOT individual file
MathStuff.adders.addTwo(7) // 9
I would definitely suggest organizing code like this. One thing this improves is testability - it's very easy to test pure functions with no side effects.
I like to put my database code in one module and import it wherever to access all my database functions.
I like to put all of my business logic in different modules by category - for instance GameLogic or something like that.
This will also help you write more functional code. Currently, you have a lot of state modification within individual functions - you won't be able to do that in modules without binding individual functions to the this context of your react component. Instead, I would suggest passing all necessary parameters to the function and having it return a value. This moves business logic away, making it easier to manage state.
For instance, your progressMission function accesses this.state.interval. You can pass interval to the function itself.
One thing I'm noticing is that your code has a lot of dependency on each other - functions often have to access lots of things outside of itself, rather than being self-contained. It would probably help you a lot to try to refactor into a modular system, where functions are much more pure - only accessing what is passed to them, and returning values which get used. Using actual modules like above definitely helps do that - my code got better the more I did it. It helps you reason about your code better. Additionally, once/if you start implementing tests, you'll find that all of the tangled-ness of the code makes it hard to test - there are a lot of side effects.
Finally, redux and external state management probably won't help a ton in your case, but they might. Redux can help you achieve state that's easier to reason about, but it won't help you organize code better per se. I hope that helps!

Related

Aurelia check clientWidth in attached event

I'm trying to find the appropriate event in the aurelia life cycle to check elements absolute width (to know if they are too narrow to insert text in them).
I thought that the attached event is the right one (where the model is attached to the DOM), but then I get that the elements width is 1:
<template>
<div ref="notCompliantDiv" css="width:${displayData.notCompliant}%;">${notCompliantText}</div>
</template>
attached() {
if (this.notCompliantDiv.clientWidth < 10)
{
this.notCompliantText = '';
}
}
The issue here is the HTML has attached itself to the page, but the element you're querying has a css attribute on it which is initialised by Aurelia itself. You're going to want to use the TaskQueue for this one.
The task queue will push the execution of your code to the bottom of the stack and run after Aurelia has done any of its dynamic logic over custom attributes and whatnot.
import { inject, TaskQueue } from 'aurelia-framework';
#inject(TaskQueue)
export class MyClass {
constructor(taskQueue) {
this.taskQueue = taskQueue;
}
attached() {
this.taskQueue.queueMicroTask(() => {
if (this.notCompliantDiv.clientWidth < 10) {
this.notCompliantText = '';
}
});
}
}

Helper not rerun when property changes

I'm trying to have a helper output nutrient totals based on a list (array) of ingredients. Since I want to display the totals of one of many nutrients I need to somehow pass it a parameter that defines the nutrient in question. So I figured a helper would be the way to go, something like this:
{{nutrient-total list "kcal"}}
The problem is that the helper is only rendered/run once. However, the {{#each}} helper is updated when a new item is pushed to the list so it seems to be possible. I think I am missing something here. Should helpers be run again if a parameter changes, or should I be trying something else?
The list looks like this:
{
ingredient: {
name: 'Potato',
group: 'Veggies',
nutrients: [{
name: 'kcal',
nutritionalValue: 87
}, {
name: 'kJ',
nutritionalValue: 42
}]
},
weight: 42
}
For future reference:
The solution with a helper:
Twiddle
The solution with a computed property
Twiddle
Your list objects is plain objects but you deal with like ember object.
I mean you shouldn't use get for those.
list.forEach(item => {
let weight = item.weight;
item.ingredient.nutrients.forEach(nutrient => {
if (nutrient.name === unit) {
total += weight * nutrient.nutritionalValue;
}
});
});
But if you want to work for all types, use Ember.get like this :
const {get} = Ember;
...
list.forEach(item => {
let weight = get(item, 'weight');
get(get(item, 'ingredient'), 'nutrients').forEach(nutrient => {
if (get(nutrient, 'name') === unit) {
total += weight * get(nutrient, 'nutritionalValue');
}
});
});
UPDATE
The main reason that helper doesn't work is that the list property should be notified.
In action you need to call this.notifyPropertyChange('list'); at the end.
Also to get rid of this you can create a class based helper
Instead of helper you can very well use computed property,
total: Ember.computed('list.[]','unit', function() {
let total = 0;
list.forEach(item => {
let weight = item.get('weight');
item.ingredient.get('nutrients').forEach(nutrient => {
if (nutrient.name === unit) {
total += weight * nutrient.nutritionalValue;
}
});
});
return total;
})
Reason for not running the helper might be, we are not changing the reference of list

Ember ember-views.render-double-modify deprecation

I am trying to eliminate a deprecation that I recently introduced into my code and I need some help.
The deprecation is:
You modified ... twice in a single render. This was unreliable in Ember 1.x and will be removed in Ember 2.0 [deprecation id: ember-views.render-double-modify]
What I'm trying to do is display a list of scores and then the total of the scores. I need to drop the lowest score and not include it into the calculated total. I have this working. The deprecation comes when I try to add a CSS class via the classNameBindings to the score that has been excluded.
I'm pretty sure this is happening when I am doing the Ember.set during my computed property of calculatedTotal.
My question is how else can I keep the total score updated with the CSS also updated when I change a score in the form ?
code details:
I have two components; score-row and judge-row. Score-row takes in an array of score objects, loops through each score calling the judge-score component.
Ember : 2.2.0
Ember Data : 2.2.1
Update Here is a working Ember Twiddle demonstrating the problem:
https://ember-twiddle.com/6696d907b604aa750201?numColumns=1
index.js - (mocked code pulled out for this question)
let scores = new Ember A();
scores.pushObject(Ember.Object.create({ points: 1 }));
scores.pushObject(Ember.Object.create({ points: 2 }));
scores.pushObject(Ember.Object.create({ points: 3 }));
index.hbs
{{score-row scores=scores}}
score-row.hbs
{{#each scores as |score|}}
{{judge-score score=score}}
{{/each}}
{{calculatedTotal}}
score-row.js:
calculatedTotal: Ember.computed('scores.#each.points', () => {
let totalScore = 0,
scores = this.get('scores');
if(Ember.isPresent(scores)) {
var i,
scoresLength = scores.length,
sortedAscending,
numLowToDrop = 1;
sortedAscending = this.get('sortedScores');
for(i = 0; i < scoresLength; i++) {
currentScoreObj = sortedAscending.objectAt(i);
// I think this is what is causing the ember-views.render-double-modify
Ember.set(currentScoreObj, '_droppedLow', (i < numLowToDrop));
}
sortedAscending.forEach((score) => {
if( !score.get('_droppedLow') ) {
totalScore += +score.get('points');
}
});
}
return totalScore;
}),
// had to write my own sort because scores.sortBy('points') was sorting as if
// points were a string and not a number ?
sortedScores: Ember.computed.sort('scores.#each.points', (score1, score2) => {
if (+score1.get('points') > +score2.get('points')) {
return 1;
} else if (+score1.get('points') < +score2.get('points')) {
return -1;
}
return 0;
})
judge-score.hbs
{{input value=score.points}}
judge-score.js
import Ember from 'ember';
export default Ember.Component.extend({
classNameBindings: [
"score._droppedLow:droppedLow"
]
});
Thanks!
I wonder if maybe each time pushObject is called it re-triggers your computed property?
Try this:
let scores = new Ember A([
Ember.Object.create({ points: 1 }),
Ember.Object.create({ points: 1 })
]);

Access has Many via foreach in computed property in ember

So I've got a hasMany defined as so
quests: DS.hasMany('quest')
I have a property that is supposed to go through each quest and work out the total.
The function looks like this,
questXP: function() {
var amount = 0;
this.get('quests').forEach(function(item)
{
console.log(item.get('xpReward'));
amount += parseInt(item.get('xpReward'));
});
return amount;
}.property('quests'),
I've tried adding async: true to the hasMany but it stops the forEach from working at all. At the moment it loops 3 times(I have 3 quests) but it isn't able to access any of the quest's properties.
My thought is it's related to the fact that the quests are still being loaded.
Any ideas on what I'm doing wrong?
Your computed property depends of each xpReward property. So you need to use quests.#each.xpReward istead of quests.
questXP: function() {
var amount = 0;
this.get('quests').forEach(function(item)
{
console.log(item.get('xpReward'));
amount += parseInt(item.get('xpReward'));
});
return amount;
}.property('quests.#each.xpReward'),

How to solve code smell: Computed property for output formatting

Hi i have this ember model. The likeMessage computed Property is building a string that i am using in my template. (see below)
This works, but i'm not comfortable having this "view layer code" in my model. What would be a better approach?
/**
* #class
* #name Entry
*/
James.Entry = Ember.Object.extend(
/** #lends Entry# */
{
likes: [],
likeMessage: function() {
var likes = this.get("likes"),
withNameCount = 0,
names = [],
likeCount = likes.length;
for(;withNameCount < likes.length && withNameCount < 2; withNameCount++) {
names.push(likes[withNameCount].name);
}
if(likeCount == 0) {
return "Nobody likes this";
} else if(likeCount == 1) {
return names[0]+ " likes this";
} else if(likeCount <= 2) {
return names.join(" and ")+" like this";
} else {
return names.join(", ")+" and "+(likes.length-2)+" others like this";
}
}.property("likes")
}
);
My template:
Likes:
{{likeMessage}}
Computed properties that are for presentation belong in the controller layer in Ember. To get easy access to the model, you can use an Ember.ObjectController and set the content property to the model instance you are showing.
I recently put together a presentation recently about what goes where when architecting an Ember app. Perhaps it could be useful: http://www.lukemelia.com/blog/archives/2012/08/23/architecting-ember-js-apps/
You could move that method pretty much as is to an EntryView and then call view.likeMessage in the template.
Without knowing your View classes I cannot be more specific but typically you would have a corresponding view for every model (often two, one "singular" and one "plural" for handling collections or single instances of the model).

Resources