This repo is a companion to this video:
Beyond having npm and node (latest versions), you should have Rust installed. I recommend nightly because living on the edge is fun.
Also recommend installing near-cli globally
npm i -g near-cli
Everything else can be installed via:
yarn
There is only one config.js file found in src/config.js
, this is also used for running tests.
Using src/config.js
you can set up your different environments. Use REACT_APP_ENV
to switch environments e.g. in package.json
script deploy
.
You can run unit tests in the Rust contracts themselves, but it may be more useful to JS tests against testnet itself.
Note: to run the app and server tests make sure you install and start the server.
- cd server
- yarn && yarn start
Commands:
test
will simply run app tests against the contract already deployed. You can mess around withapp.test.js
and try different frontend stufftest:deploy
- will deploy a new dev account (/neardev
) and deploy a new contract to this account, then runtest
test:unit
- runs the rust unit tests
If you've changed your contract or your dev account has run out of funds use test:deploy
, if you're updating your JS tests only then use test
.
There are helpers in test/test-utils.js
that take care of:
- creating a near connection and establishing a keystore for the dev account
- creating test accounts each time a test is run
- establishing a contract instance so you can call methods
You can change the default funding amount for test accounts in src/config.js
In src/state/near.js
you will see that src/config.js
is loaded as a function. This is to satisfy the jest/node test runner.
You can destructure any properies of the config easily in any module you import it in like this:
// example file app.js
import getConfig from '../config';
export const {
GAS,
networkId, nodeUrl, walletUrl, nameSuffix,
contractName,
} = getConfig();
Note the export const in the destructuring?
Now you can import these like so:
//example file Component.js
import { GAS } from '../app.js'
...
await contract.withdraw({ amount: parseNearAmount('1') }, GAS)
...
- Bundled with Parcel 2.0 (@next) && eslint
- Minimal all-in-one state management with async/await support
The following steps describe how to use
src/utils/state
to create and use your ownstore
andStateProvider
.
- Create a file e.g.
/state/app.js
and add the following code
import { State } from '../utils/state';
// example
const initialState = {
app: {
mounted: false
}
};
export const { store, Provider } = State(initialState);
- Now in your
index.js
wrap yourApp
component with theStateProvider
import { Provider } from './state/app';
ReactDOM.render(
<Provider>
<App />
</Provider>,
document.getElementById('root')
);
- Finally in
App.js
you canuseContext(store)
const { state, dispatch, update } = useContext(store);
<p>Hello {state.foo && state.foo.bar.hello}</p>
const handleClick = () => {
update('clicked', !state.clicked);
};
const onMount = () => {
dispatch(onAppMount('world'));
};
useEffect(onMount, []);
When a function is called using dispatch, it expects arguments passed in to the outer function and the inner function returned to be async with the following json args: { update, getState, dispatch }
Example of a call:
dispatch(onAppMount('world'));
All dispatched methods and update calls are async and can be awaited. It also doesn't matter what file/module the functions are in, since the json args provide all the context needed for updates to state.
For example:
import { helloWorld } from './hello';
export const onAppMount = (message) => async ({ update, getState, dispatch }) => {
update('app', { mounted: true });
update('clicked', false);
update('data', { mounted: true });
await update('', { data: { mounted: false } });
console.log('getState', getState());
update('foo.bar', { hello: true });
update('foo.bar', { hello: false, goodbye: true });
update('foo', { bar: { hello: true, goodbye: false } });
update('foo.bar.goodbye', true);
await new Promise((resolve) => setTimeout(() => {
console.log('getState', getState());
resolve();
}, 2000));
dispatch(helloWorld(message));
};
The default names the State
factory method returns are store
and Provider
. However, if you want multiple stores and provider contexts you can pass an additional prefix
argument to disambiguate.
export const { appStore, AppProvider } = State(initialState, 'app');
The updating of a single store, even several levels down, is quite quick. If you're worried about components re-rendering, use memo
:
import React, { memo } from 'react';
const HelloMessage = memo(({ message }) => {
console.log('rendered message');
return <p>Hello { message }</p>;
});
export default HelloMessage;
Higher up the component hierarchy you might have:
const App = () => {
const { state, dispatch, update } = useContext(appStore);
...
const handleClick = () => {
update('clicked', !state.clicked);
};
return (
<div className="root">
<HelloMessage message={state.foo && state.foo.bar.hello} />
<p>clicked: {JSON.stringify(state.clicked)}</p>
<button onClick={handleClick}>Click Me</button>
</div>
);
};
When the button is clicked, the component HelloMessage will not re-render, it's value has been memoized (cached). Using this method you can easily prevent performance intensive state updates in further down components until they are neccessary.
Reference: