Skip to content
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

March fun react #1

Open
wants to merge 7 commits into
base: march_fun_base
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,5 @@ force-app/main/default/staticresources/App.resource

# temp
old_src
force-app-old
force-app-old
MarchFun.resource
4 changes: 2 additions & 2 deletions config/ts-force-config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"auth": {
"username": "scratch"
"username": "ralphdev"
},
"sObjects": ["Todo__c"],
"sObjects": ["Opportunity"],
"outPath": "./src/generated/sobs.ts"
}
18 changes: 18 additions & 0 deletions force-app/main/default/pages/MarchFun.page
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<apex:page showHeader="true" sidebar="false">
<script type="text/javascript">
//rest details
const __ACCESSTOKEN__ = '{!$Api.Session_ID}';
const __RESTHOST__ = '';
</script>
<apex:stylesheet value="https://npmcdn.com/antd/dist/antd.css"/>

<div id="root"></div>
<apex:outputPanel layout="none" rendered="{!$CurrentPage.parameters.local != '1'}">
<script type='text/javascript' src="{!URLFOR($Resource.MarchFun, 'app.js')}" />
<script type='application/json' src="{!URLFOR($Resource.MarchFun, 'app.js.map')}" />
</apex:outputPanel>

<apex:outputPanel layout="none" rendered="{!$CurrentPage.parameters.local == '1'}">
<script src="http://localhost:8080/app.js" />
</apex:outputPanel>
</apex:page>
12 changes: 12 additions & 0 deletions force-app/main/default/pages/MarchFun.page-meta.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexPage xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>38.0</apiVersion>
<availableInTouch>false</availableInTouch>
<confirmationTokenRequired>false</confirmationTokenRequired>
<label>MarchFun</label>
<packageVersions>
<majorNumber>1</majorNumber>
<minorNumber>7</minorNumber>
<namespace>sf_com_apps</namespace>
</packageVersions>
</ApexPage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<StaticResource xmlns="http://soap.sforce.com/2006/04/metadata">
<cacheControl>Public</cacheControl>
<contentType>application/zip</contentType>
<description>ReactApp</description>
</StaticResource>
82 changes: 41 additions & 41 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"make-prod-default": "sfdx force:config:set defaultusername=$npm_config_prod_alias",
"bundle-app-dev": "npm run compile-dev && npm run copy-bundle",
"bundle-app-prod": "npm run compile-prod && npm run copy-bundle",
"copy-bundle": "zip -j ./force-app/main/default/staticresources/app.zip ./dist/app.js ./dist/app.js.map && mv ./force-app/main/default/staticresources/app.zip ./force-app/main/default/staticresources/app.resource",
"copy-bundle": "zip -j ./force-app/main/default/staticresources/app.zip ./dist/app.js ./dist/app.js.map && mv ./force-app/main/default/staticresources/app.zip ./force-app/main/default/staticresources/MarchFun.resource",
"package-sfdc": "sfdx force:source:convert -d ./dist/sfdc-package",
"build-dev": "npm run bundle-app-dev && npm run package-sfdc",
"build-prod": "npm run bundle-app-prod && npm run package-sfdc",
Expand All @@ -42,61 +42,61 @@
"reinstall:win": "rd /s /q node_modules && npm"
},
"dependencies": {
"antd": "^3.0.0",
"babel-jest": "^21.0.2",
"antd": "^3.3.3",
"babel-jest": "^21.2.0",
"babel-polyfill": "^6.26.0",
"csstips": "^0.2.0",
"immutability-helper": "^2.4.0",
"lodash": "^4.17.4",
"query-string": "^5.0.0",
"csstips": "^0.2.3",
"immutability-helper": "^2.6.6",
"lodash": "^4.17.5",
"query-string": "^5.1.1",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-redux": "^5.0.6",
"react-redux": "^5.0.7",
"react-router-dom": "^4.1.1",
"redux": "^3.6.0",
"redux-devtools-extension": "^2.13.2",
"redux-thunk": "^2.2.0",
"ts-force": "^1.0.32",
"tslib": "^1.6.1",
"typestyle": "^1.3.2"
"ts-force": "^1.0.35",
"tslib": "^1.9.0",
"typestyle": "^1.7.2"
},
"devDependencies": {
"@types/enzyme": "^3.1.5",
"@types/enzyme-adapter-react-16": "^1.0.1",
"@types/jest": "^21.1.8",
"@types/node": "^7.0.14",
"@types/react": "^16.0.26",
"@types/react-dom": "^16.0.3",
"@types/react-hot-loader": "^3.0.1",
"@types/react-redux": "^5.0.9",
"@types/redux": "^3.6.0",
"@types/redux-thunk": "^2.1.0",
"@types/webpack": "^3.5.5",
"@types/webpack-dev-server": "^2.3.0",
"@types/webpack-env": "^1.13.0",
"awesome-typescript-loader": "^3.1.3",
"babel-loader": "^7.1.2",
"babel-plugin-import": "^1.6.0",
"@types/enzyme": "^3.1.9",
"@types/enzyme-adapter-react-16": "^1.0.2",
"@types/jest": "^21.1.10",
"@types/node": "^7.0.58",
"@types/react": "^16.1.0",
"@types/react-dom": "^16.0.4",
"@types/react-hot-loader": "^3.0.6",
"@types/react-redux": "^5.0.15",
"@types/redux": "^3.6.31",
"@types/redux-thunk": "^2.1.32",
"@types/webpack": "^3.8.11",
"@types/webpack-dev-server": "^2.9.4",
"@types/webpack-env": "^1.13.5",
"awesome-typescript-loader": "^3.5.0",
"babel-loader": "^7.1.4",
"babel-plugin-import": "^1.6.7",
"babel-preset-es2015": "^6.24.1",
"css-loader": "^0.28.7",
"enzyme": "^3.2.0",
"enzyme-adapter-react-16": "^1.1.0",
"css-loader": "^0.28.11",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"html-webpack-plugin": "^2.28.0",
"husky": "^0.13.3",
"jest": "^21.2.1",
"less": "^2.7.2",
"less-loader": "^4.0.5",
"less-vars-to-js": "^1.2.0",
"react-hot-loader": "^3.0.0-beta.7",
"react-test-renderer": "^15.6.1",
"less": "^2.7.3",
"less-loader": "^4.1.0",
"less-vars-to-js": "^1.2.1",
"react-hot-loader": "^3.1.3",
"react-test-renderer": "^15.6.2",
"rimraf": "^2.6.1",
"style-loader": "^0.18.2",
"ts-jest": "^21.2.3",
"tslint": "^5.1.0",
"tslint-react": "^3.0.0",
"typescript": "^2.6.2",
"webpack": "^3.5.5",
"webpack-dashboard": "^1.0.0",
"webpack-dev-server": "^2.3.0"
"tslint": "^5.9.1",
"tslint-react": "^3.5.1",
"typescript": "^2.8.1",
"webpack": "^3.11.0",
"webpack-dashboard": "^1.1.1",
"webpack-dev-server": "^2.11.2"
}
}
105 changes: 105 additions & 0 deletions src/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// tslint:disable-next-line:no-implicit-dependencies
import { Opportunity } from "@src/generated/sobs";
import { Affix, Button, Col, Icon, Layout, Row, Spin } from "antd";
import * as React from "react";

interface Props {
id: string;
}

interface State {
isOpptyLoading: boolean;
isTouchSaving: boolean;
oppName: string;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could just store the OpportunityFeilds directly on the state and then when you need to update state, use ES6 spread syntax. to do so without mutating:

let newOppty = {...this.state.oppty, ...{touches: newCount}};
await new Opportunity(newOppty).update(); 
this.setState({oppty: newOppty});

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so much cool syntax

in your experience has storing the full sobject (the immutable fields instance) worked out better than storing just the data points you need? seems like it would be less to manage, but could invite extra functions you didn't intend (or maybe that's a positive since you don't need to adjust your state logic every time you want to start referring to a new field on a page)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ralphcallaway that's a great question.

There's definitely a trade off to each. It's MUCH easier and efficient (productivity wise) to just store the entire object. When you are rapidly prototyping, it makes things come together really fast.

However, it violates Idiomatic redux for two reasons:

1: the SObjects returned by ts-force are classes and have functions, which apparently can have negative impacts on performance (you are only suppose to store POJOS).

2: ts-force allows you to pull dependency trees down and they are all stored on the object. This violates the best practice of normalizing your reducers. You could easily use a library like normalizr to solve this issue

My approach has been to follow this simple rule:

It's ok to store SObjects in the reducer ONLY when you don't need to mutate the objects themselves. It's probably not a huge deal to change first class properties, but definitely shouldn't change children objects

The only downside has been that my state size has gotten really big, due to duplicate relationship data being pulled down. For example, I'm storing treatments on every Spotlight Product. There might be 5000 products in my store, each with object data, but there may only be 15 distinct treatments across all of them.

I don't think this has a big impact on rendering performance, but it does make things like trying to store state in Saleforce field harder (I currently need 10 long text fields to be safe!).

touchCount: number;
}

const loadingIndicator = (
<Affix offsetTop={300}>
<Row type="flex" justify="center">
<Col span={1}>
<Icon spin={true} style={{ fontSize: 56 }} type="loading" />
</Col>
</Row>
</Affix>
);

export class App extends React.Component<Props, State> {
constructor(props) {
super(props);
this.state = {
oppName: "Loading",
isOpptyLoading: true,
isTouchSaving: false,
touchCount: 0,
};
this.touchOppty = this.touchOppty.bind(this);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ralphcallaway I'm sure you learned this in the react training, but you can use => functions instead of bind! It's much cleaner approach IMO. Basically all you need to do is change:

public async touchOppty() {...}

to

public touchOppty = async () => {...}

then you don't have to bind to this in the constructor

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ChuckJonas so i'm a bit confused, I thought that wasn't advisable since it would lead to the event handler function being re-initialized everytime the component was rendered ...

came to it with this link https://stackoverflow.com/questions/36677733/why-shouldnt-jsx-props-use-arrow-functions-or-bind in your tslint.json file

does that not apply if I do thinks like you're describing?

}

public async touchOppty() {
const newCount = this.state.touchCount + 1;
this.setState({ isTouchSaving: true });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads up: setState is actually asynchronous. It's actually a pretty common bug people create and can cause confusion.

I doubt it would ever cause an issue here, because the latency behind the HTTP Callout & and the fact that there isn't really any impact to setting isTouchSaving: false, but there are instances were you end up overwriting state in a bad way. A simple fix would be to pass the dependent state update in as a function into the setState callback

this.setState({ isTouchSaving: true }, ()=>{
    //guaranteed to have isTouchSaving == true
});
//not guaranteed isTouchSaving == true

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

woah, okay, did not realize that. do you know if it's safe to assume that set state changes are enqueued in order?

i.e. in this case the hypothetical race condition would be, enqueue state isTouchSaving: true, make api call, enqueue state isTouchSaving: false. as long as the first state change always come through first seems like we'd be good in this scenario. in other words, as long downstream code isn't reading the state in a way that relies on the initially step being completed we could stay blissfully ignorant of this ...

const oppty = new Opportunity(); // ts-lint likes const, but we're still mutating?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It recommends const because oppty never changes. EG: if you ran oppty = new Opportunity(); again, you would now be assigning oppty to a new location (which const wouldn't allow).

const has no impact the object properties, as these are technically really just structs (each property get it's own memory assignment).

IF you did want immutability, you have a couple options.

You could use the Readonly mapped type:

const oppty :Readonly<Opportunity> = new Opportunity();
oppty.name = 'abc' //Cannot assign to 'name' because it is a constant or a read-only property.
oppty.account.name = 'abc' //no error thrown :0

However, as you can see, it only makes the object first class properties immutable (typescript 2.8condition types now support DeepReadonly but it doesn't come packaged in ts yet).

In order to provide immutable piece of mind, ts-force generates a readonly & partial interfaces for each object: [ObjectName]Fields. So in this case, if you wanted full immutability, you can do this:

const oppty :OpportunityFields = new Opportunity();
 oppty.name = 'abc' //Cannot assign to 'name' because it is a constant or a read-only property.
 oppty.account.name = 'abc' //Cannot assign to 'name' because it is a constant or a read-only property.

If I use an SObject directly in my reducer I make sure it's typed to this "Fields" interface so I don't accidentally mutate.

Then, if you need to mutate and save, you can simply pass it back into the constructor of the corresponding class:

const mutatableOpp = new Opportunity(oppty);
mutatableOpp.name = 'asdf';
mutatableOpp.update();

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that makes sense, thanks for the detailed explanation, that really clarified things, const just means same reference, if you need read only objects you've got some options (i also saw Object.freeze as something that could be used)

oppty.id = this.props.id;
oppty.touches = newCount;
try {
await oppty.update(true);
this.setState({
isTouchSaving: false,
touchCount: oppty.touches,
});
} catch (err) {
alert(`Error touching oppty: ${err}`);
}
}

public async componentDidMount() {
try {
const oppRetrieve = await Opportunity.retrieve(
`SELECT Name, Touches__c FROM Opportunity WHERE Id = '${this.props.id}'`);
if (oppRetrieve.length > 0) {
this.setState({
isOpptyLoading: false,
touchCount: oppRetrieve[0].touches,
oppName: oppRetrieve[0].name,
});
} else {
this.setState({
isOpptyLoading: false,
oppName: `Couldn't find opportunity with id ${this.props.id}`,
});
}
} catch (err) {
this.setState({
isOpptyLoading: false,
oppName: `Error loading opportunity "${err}"`,
});
}
}

public render() {
return (
<Layout>
<Spin indicator={loadingIndicator} spinning={this.state.isOpptyLoading}>
<Layout.Header>
<h1>Opportunity Toucher: "{this.state.oppName}"</h1>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Best Practices would want you to break these out into stateless components. The rule of thumb is:

If it has state, it shouldn't output any HTML

antd makes this much easier to achieve because it's vastly reduces the boiler plate HTML you'll need.

In general, I think this is a very important rule to help separate presentation logic away from business logic, but in this simple case would definitely be overkill IMO.
This article on Presentational and Container Components does a great job of explaining why.

We also have a skill challenge for just this (however, it's not great IMO)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

awesome, that rule of thumb is great, makes applying it really easy.

</Layout.Header>
<Layout.Content>
<h1 style={{marginLeft: "50px", marginTop: "0.67em"}}>
Touches: {this.state.touchCount}
</h1>
</Layout.Content>
<Layout.Footer>
<Button
type="primary"
onClick={this.touchOppty}
loading={this.state.isTouchSaving}
>
Touch Opportunity
</Button>
</Layout.Footer>
</Spin>
</Layout>
);
}
}
Loading