Binding to Higher Order Component from aws-amplify - interop

What is bucklescript looking for satisfy the Functions are not valid as a React child. error being produced by the following example.
I have this binding to withAuthenticator from aws-amplify-react.
[#bs.deriving abstract]
type props = {
[#bs.as "Comp"]
comp: React.element,
[#bs.optional] includeGreetings: bool,
};
[#genType.import ("aws-amplify-react", "withAuthenticator")] [#react.component]
external make:(
~props:props,
) => React.element = "withAuthenticator";
let default = make;
In Demo.re I use the binding as follows:
let props = {
WithAuthenticator.props(
~comp={
<App />;
},
~includeGreetings=true,
(),
);
};
Js.log(props);
[#react.component]
let app = () => <WithAuthenticator props />;
Then in App.js I use Demo.re like so:
import Amplify from 'aws-amplify';
import {app as App } from './Demo.bs';
import awsconfig from './aws-exports';
import './App.css';
Amplify.configure(awsconfig);
export default App;
Which produces the following error:
Warning: Functions are not valid as a React child. This may happen if you return a Component instead of <Component /> from render. Or maybe you meant to call this function rather than return it.
in withAuthenticator (created by Demo$app)
in Demo$app (at src/index.js:7)
I would like to understand what this means in order to deal with when it comes up again.
This is what the compiled bucklescript code is in Demo.bs.js:
// Generated by BUCKLESCRIPT, PLEASE EDIT WITH CARE
'use strict';
var React = require("react");
var App$ReactHooksTemplate = require("./App.bs.js");
var WithAuthenticator$ReactHooksTemplate = require("../aws/WithAuthenticator.bs.js");
var props = {
Comp: React.createElement(App$ReactHooksTemplate.make, { }),
includeGreetings: true
};
console.log(props);
function Demo$app(Props) {
return React.createElement(WithAuthenticator$ReactHooksTemplate.make, {
props: props
});
}
var app = Demo$app;
exports.props = props;
exports.app = app;
/* props Not a pure module */
Reproduction of this issue can be found here.
Update:
Here I am trying to follow up on #glennsl's comments/answer below.
// define a type modeling what `withAuthenticator` is expecting
[#bs.deriving abstract]
type props = {
[#bs.as "Comp"]
comp: React.element,
[#bs.optional]
includeGreetings: bool,
};
// use bs.module instead of gentype
[#bs.module ("aws-amplify-react", "withAuthenticator")]
external withAuthenticator: props => React.component(props) =
"withAuthenticator";
module AppWithAuthenticator = {
[#bs.obj]
external makeProps:
(~children: 'children, unit) => {. "children": 'children} =
"";
let make = props => withAuthenticator(props);
};
This is how it might be used, but doesnt compile.
module AppWithAuth = {
let props = {
props(
~comp={
<App />;
},
~includeGreetings=true,
(),
);
};
[#react.component]
let make = () => {
<AppWithAuthenticator props />;
};
};
compile error:
>>>> Start compiling
[1/3] Building src/aws/AuthenticatorBS-ReactHooksTemplate.cmj
We've found a bug for you!
/Users/prisc_000/working/DEMOS/my-app/src/aws/AuthenticatorBS.re 34:6-25
32 │ [#react.component]
33 │ let make = () => {
34 │ <AppWithAuthenticator props />;
35 │ };
36 │ };
This call is missing an argument of type props

Something along these lines should work:
[#genType.import ("aws-amplify-react", "withAuthenticator")]
external withAuthenticator : (React.component('a), bool) => React.component('a) = "withAuthenticator";
module AppWithAuthenticator = {
[#bs.obj]
external makeProps: (~children: 'children=?, unit) => {. "children": 'children } = "";
let make = withAuthenticator(App.make, true);
};
ReactDOMRe.renderToElementWithId(<AppWithAuthenticator />, "root");
external withAuthenticator : ... declares the external HOC constructor as a function that takes a react component and a bool, and returns a component that will accept the exact same props due to the 'a type variable being used in both positions.
module AppWithAuthenticator ... applies the HOC constructor to the App component and sets it up so that it can be used with JSX. This is basically the same as importing a react component directly, except we get the external component by way of a function call instead of importing it directly.
Finally, the last line just demonstrates how it could be used.
Note that I obviously haven't tested this properly as I don't have a project set up with aws-amplify and such. I've also never used genType, but it seems pretty straightforward for this use case.

Reason discord channel strikes again. This solution works:
[#bs.module "aws-amplify-react"]
external withAuthenticator:
// takes a react component and returns a react component with the same signature
React.component('props) => React.component('props) =
"withAuthenticator";
module App = {
[#react.component]
let make = (~message) => <div> message->React.string </div>;
};
module WrappedApp = {
include App;
let make = withAuthenticator(make);
};
And if you want to pass the second includeGreeting prop like in #glennsl's answer:
[#bs.module "aws-amplify-react"]
external withAuthenticator:
// takes a react component and returns a react component with the same signature
(React.component('props), bool) => React.component('props) =
"withAuthenticator";
module App = {
[#react.component]
let make = (~message) => <div> message->React.string </div>;
};
module WrappedApp = {
include App;
let make = withAuthenticator(make,true);
};
You would call it with:
ReactDOMRe.renderToElementWithId(<WrappedApp message="Thanks" />, "root");
Thanks to #bloodyowl.
And this is what it looks like if you don't use include. See #glennsl's comment below.
module WrappedApp = {
let makeProps = App.makeProps;
let make = withAuthenticator(App.make,true);
};

Related

Rendered more hooks than during the previous render

How to use 2 graphql queries with react-apollo-hooks where the 2nd query depends on a parameter retrieved from the 1st query?
I try to use 2 queries which looks like this:
const [o, setO] = useState()
const { loading: loadingO, error: errorO, data: dataO } = useQuery(Q_GET_O, { onCompleted: d => setO(d.getO[0].id) });
if (loadingO) { return "error" }
const { loading: loadingOP, error: errorOP, data: dataOP } = useQuery(Q_GET_OP, { variables: { o } })
However, when I run my project, react-hooks gives me the following message:
"index.js:1437 Warning: React has detected a change in the order of Hooks called by Upgrade. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks"
I would like to know how I can use react-apollo-hooks in order to run a query that depends on another query. It works great if the graphql query variables are known in advance. However, I did not find a solution for variables that come from other query.
The problem here is that you are short circuit returning before all of your hooks have a chance to run.
React will complain if you exit a render function before all of the hooks have a chance to be called.
For example:
function BrokenFoo () {
const query = useSomeQuery();
if (query.loading) return <Loading />
// This will cause some issues because
// it's possible that we return before our useState hook gets called
const [bar, setBar] = useState();
return <SomeComponent bar={bar} setBar={setBar} data={query.data} />
}
To fix:
function FixedFoo () {
// This will be fine because
// all of the hooks have a chance to be called before a return
const query = useSomeQuery();
const [bar, setBar] = useState();
if (query.loading) return <Loading />
return <SomeComponent bar={bar} setBar={setBar} data={query.data} />
}
You can add the skip option to the second query and lose the if condition:
const { loading: loadingOP, error: errorOP, data: dataOP }
= useQuery(Q_GET_OP, { variables: { o }, skip: !o })
from the docs:
If skip is true, the query will be skipped entirely

React-Redux re-render on dispatch inside HOC not working

I am busy with a little proof of concept where basically the requirement is to have the home page be a login screen when a user has not logged in yet, after which a component with the relevant content is shown instead when the state changes upon successful authentication.
I have to state upfront that I am very new to react and redux and am busy working through a tutorial to get my skills up. However, this tutorial is a bit basic in the sense that it doesn't deal with connecting with a server to get stuff done on it.
My first problem was to get props to be available in the context of the last then of a fetch as I was getting an error that this.props.dispatch was undefined. I used the old javascript trick around that and if I put a console.log in the final then, I can see it is no longer undefined and actually a function as expected.
The problem for me now is that nothing happens when dispatch is called. However, if I manually refresh the page it will display the AuthenticatedPartialPage component as expected because the localstorage got populated.
My understanding is that on dispatch being called, the conditional statement will be reavaluated and AuthenticatedPartialPage should display.
It feels like something is missing, that the dispatch isn't communicating the change back to the parent component and thus nothing happens. Is this correct, and if so, how would I go about wiring up that piece of code?
The HomePage HOC:
import React from 'react';
import { createStore, combineReducers } from 'redux';
import { connect } from 'react-redux';
import AuthenticatedPartialPage from './partials/home-page/authenticated';
import AnonymousPartialPage from './partials/home-page/anonymous';
import { loggedIntoApi, logOutOfApi } from '../actions/authentication';
import authReducer from '../reducers/authentication'
// unconnected stateless react component
const HomePage = (props) => (
<div>
{ !props.auth
? <AnonymousPartialPage />
: <AuthenticatedPartialPage /> }
</div>
);
const mapStateToProps = (state) => {
const store = createStore(
combineReducers({
auth: authReducer
})
);
// When the user logs in, in the Anonymous component, the local storage is set with the response
// of the API when the log in attempt was successful.
const storageAuth = JSON.parse(localStorage.getItem('auth'));
if(storageAuth !== null) {
// Clear auth state in case local storage has been cleaned and thus the user should not be logged in.
store.dispatch(logOutOfApi());
// Make sure the auth info in local storage is contained in the state.auth object.
store.dispatch(loggedIntoApi(...storageAuth))
}
return {
auth: state.auth && state.auth.jwt && storageAuth === null
? state.auth
: storageAuth
};
}
export default connect(mapStateToProps)(HomePage);
with the Anonymous LOC being:
import React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { loggedIntoApi } from '../../../actions/authentication';
export class AnonymousPartialPage extends React.Component {
constructor(props) {
super(props);
}
onSubmit = (e) => {
e.preventDefault();
const loginData = { ... };
// This is where I thought the problem initially occurred as I
// would get an error that `this.props` was undefined in the final
// then` of the `fetch`. After doing this, however, the error went
// away and I can see that `props.dispatch is no longer undefined
// when using it. Now though, nothing happens.
const props = this.props;
fetch('https://.../api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(loginData)
})
.then(function(response) {
return response.json();
})
.then(function(data) {
if(data && data.jwt) {
props.dispatch(loggedIntoApi(data));
localStorage.setItem('auth', JSON.stringify(data));
}
// else show an error on screen
});
};
render() {
return (
<div>
... onSubmit gets called successfully somewhere in here ...
</div>
);
}
}
export default connect()(AnonymousPartialPage);
the action:
// LOGGED_INTO_API
export const loggedIntoApi = (auth_token) => ({
type: 'LOGGED_INTO_API',
auth: auth_token
});
// LOGGED_OUT_OF_API
export const logOutOfApi = (j) => ({
type: 'LOG_OUT_OF_API'
});
and finally the reducer:
const authDefaultState = { };
export default (state = authDefaultState, action) => {
switch (action.type) {
case 'LOGGED_INTO_API':
// SOLUTION : changed this line "return action.auth;" to this:
return { ...action.auth, time_stamp: new Date().getTime() }
case 'LOG_OUT_OF_API':
return { auth: authDefaultState };
default:
return state;
}
};
My suggestion would be to make sure that the state that you are changing inside Redux is changing according to javascript's equality operator!. There is a really good answer to another question posted that captures this idea here. Basically, you can't mutate an old object and send it back to Redux and hope it will re-render because the equality check with old object will return TRUE and thus Redux thinks that nothing changed! I had to solve this issue by creating an entirely new object with the updated values and sending it through dispatch().
Essentially:
x = {
foo:bar
}
x.foo = "baz"
dispatch(thereWasAChange(x)) // doesn't update because the x_old === x returns TRUE!
Instead I created a new object:
x = {
foo:"bar"
}
y = JSON.parse(JSON.stringify(x)) // creates an entirely new object
dispatch(thereWasAChange(y)) // now it should update x correctly and trigger a rerender
// BE CAREFUL OF THE FOLLOWING!
y = x
dispatch(thereWasAChange(y)) // This WON'T work!!, both y and x reference the SAME OBJECT! and therefore will not trigger a rerender
Hope this helps!

Redux + storybook throws warning about changing store on the fly even with module.hot implemtended

I'm using storybook and I want to add redux as decorator.
Whe running storybook, I got warning in console:
<Provider> does not support changing `store` on the fly. It is most likely that you see this error because you updated to Redux 2.x and React Redux 2.x which no longer hot reload reducers automatically. See https://github.com/reactjs/react-redux/releases/tag/v2.0.0 for the migration instructions.
It's my code for config storybook:
/* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions */
import React from 'react';
import { configure, storiesOf } from '#storybook/react';
import { Provider as ReduxProvider } from 'react-redux';
import forEach from 'lodash/forEach';
import unset from 'lodash/unset';
import Provider from 'components/Provider';
import initStore from 'utils/initStore';
import messages from '../lang/en.json';
const req = require.context('../components', true, /_stories\.js$/);
const ProviderDecorator = (storyFn) => {
const TheProvider = Provider(() => storyFn());
return (
<ReduxProvider store={initStore()}>
<TheProvider key={Math.random()} now={1499149917064} locale="en" messages={messages} />
</ReduxProvider>
);
}
function loadStories() {
req.keys().forEach((filename) => {
const data = req(filename);
if (data.Component !== undefined && data.name !== undefined && data.stories !== undefined) {
const Component = data.Component;
const stories = storiesOf(data.name, module);
stories.addDecorator(ProviderDecorator);
let decorator = data.stories.__decorator;
if (data.stories.__decorator !== undefined) {
stories.addDecorator((storyFn) => data.stories.__decorator(storyFn()));
}
forEach(data.stories, (el, key) => {
if (key.indexOf('__') !== 0) {
stories.add(key, () => (
<Component {...el} />
));
}
});
} else {
console.error(`Missing test data for ${filename}!`)
}
});
}
configure(loadStories, module);
and initStore file:
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import thunkMiddleware from 'redux-thunk';
import { persistStore, autoRehydrate } from 'redux-persist';
import reducers from 'containers/redux/reducers';
export default () => {
const store = createStore(
reducers,
{},
composeWithDevTools(applyMiddleware(thunkMiddleware), autoRehydrate()),
);
if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('../containers/redux/reducers', () => {
const nextReducers = require('../containers/redux/reducers'); // eslint-disable-line global-require
store.replaceReducer(nextReducers);
});
}
persistStore(store);
return store;
};
So as you can see I followed instructions from link in warning. What have I done wrong and how can I remove this warning? I know it won't show on production server, but it's pretty annoying in dev mode. :/
The reason this is happening has to do with the way Storybook hot-loads.
When you change your story, that module is hot-loaded, meaning that the code inside it is executed again.
Since you're using a store creator function and not a store instance from another module, the actual store object that is being passed to ReduxProvider on hot-load is new every time.
However, the React tree that is re-constructed is for the most part identical, meaning that the ReduxProvider instance is re-rendered with new props instead of being re-created.
Essentially, this is changing its store on the fly.
The solve is to make sure that ReduxProvider instance is new, too, on hot-load. This is easily solved by passing it a unique key prop, e.g.:
const ProviderDecorator = (storyFn) => {
const TheProvider = Provider(() => storyFn());
return (
<ReduxProvider key={Math.random()} store={initStore()}>
<TheProvider key={Math.random()} now={1499149917064} locale="en" messages={messages} />
</ReduxProvider>
);
}
From React Keys:
Keys help React identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity.

Vuejs Unit Test - Backing Mocks with Tests

I am writing unit testing for a vuejs 2 application that uses Vuex as a store. I have the following pattern in many of my components:
example component thing.vue:
<template>
<div>
{{ thing.label }}
</div>
</template>
<script>
export default {
name: 'thing',
data() { return { } },
computed: {
thing () {
return this.$store.state.thing;
}
}
}
</script>
Example Store State:
export const state = {
thing: { label: 'test' }
};
Example Unit for Thing.vue:
describe('thing ', () => {
const storeMock = new Vuex.Store( state: { thing: { label: 'test' } } );
it('should pull thing from store', () => {
const Constructor = Vue.extend(thing);
const component new Constructor({ store }).$mount();
expect(component.thing).toEqual({ label: 'test' });
});
});
Example Unit test for Store:
import store from './store';
describe('Vuex store ', () => {
it('should have a thing object', () => {
expect(store.state.thing).toEqual({ label: 'test' });
});
});
There is a huge problem with this pattern. When another developer refractors the store state, they will see the Store test fail, but because the thing unit test is based on a mocked version of the store that test with continue to pass, even though that component will never work. There isn't a good way to know a refactor invalidated a Mock.
So how do people unit test this type of dependence?
One way would be to cheat a little on the unit test and use the real store state, but then it isn't really a unit test. The other way is rely on integration testing to catch the mock - store mismatch, but that feels like it would be painful to debug why the unit tests pass but the integration tests are failing.
What we ended up doing is using the actual store. Because the store state is just an object we figured it was acceptable.
We also use the store getters, actions and mutations as templates for jasmine spyies.
// Vuex needs polyfill
import { polyfill } from 'es6-promise';
polyfill();
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
import test from 'app/components/test.vue';
import module from 'app/store/modules/module';
describe('Spec for Test.vue', () => {
var props;
var state;
var actions;
var mutations;
var getters;
var store;
beforeEach( () => {
jasmine.addMatchers(customMatchers);
props = { };
// Don't change the modules
state = Object.assign({}, module.state);
actions = Object.assign({}, module.actions);
mutations = Object.assign({}, module.mutations);
getters = Object.assign({}, module.getters);
// Add require global actions, mutations, and getters here...
actions.globalActionHere = 'anything'; // this turns into a spy
// Update State with required fields
state.defaults = { id: 1 } // default expected when the component loads
// Replace modules copies with mocks
actions = jasmine.createSpyObj('actions', actions);
mutations = jasmine.createSpyObj('mutations', mutations);
getters = jasmine.createSpyObj('getters', getters);
store = new Vuex.Store( { state: { module: state }, getters, actions, mutations } );
} );
it('should have a name of test', () => {
const Constructor = Vue.extend(thing);
const component new Constructor({ store, props }).$mount();
expect(component.$options.name).toBe('test');
});
});
Note the part
jasmine.createSpyObj('actions', actions);
Jasmine spies will use the module to create spyies for each of the methods, which is very useful.

Testing a Redux action creator that appends a timestamp

When writing a Mocha test spec against an action creator how can I be certain what a timestamp will be if it is generated within the action creator?
It doesn't have to utilize Sinon, but I tried to make use of Sinon Fake Timers to "freeze time" and just can't seem to get this pieced together wither with my limited knowledge of stubbing and mocking. If this is considered a Redux anti-pattern please point me in a better direction, but my understanding is that Redux action creators can be non-pure functions, unlike reducers.
Borrowing a little from the Redux Writing Tests Recipes here is the core of my problem as I understand it...
CommonUtils.js
import moment from 'moment';
export const getTimestamp = function () {
return moment().format();
};
TodoActions.js
import { getTimestamp } from '../../utils/CommonUtils';
export function addTodo(text) {
return {
type: 'ADD_TODO',
text,
timestamp: getTimestamp() // <-- This is the new property
};
};
TodoActions.spec.js
import expect from 'expect';
import * as actions from '../../actions/TodoActions';
import * as types from '../../constants/ActionTypes';
import { getTimestamp } from '../../utils/CommonUtils';
describe('actions', () => {
it('should create an action to add a todo', () => {
const text = 'Finish docs';
const timestamp = getTimestamp(); // <-- This will often be off by a few milliseconds
const expectedAction = {
type: types.ADD_TODO,
text,
timestamp
};
expect(actions.addTodo(text)).toEqual(expectedAction);
});
});
When testing time I have used this library successfully in the past: https://www.npmjs.com/package/timekeeper
Then in a beforeEach and afterEach you can save the time to be something specific and make your assertions then reset the time to be normal after.
let time;
beforeEach(() => {
time = new Date(1451935054510); // 1/4/16
tk.freeze(time);
});
afterEach(() => {
tk.reset();
});
Now you can make assertions on what time is being returned. Does this make sense?
I would still love to see other answers but I finally got a reasonable solution. This answer uses proxyquire to override/replace the getTimestamp() method defined in CommonUtils when used by TodoActions for the duration of the test.
No modifications to CommonUtils.js or TodoActions.js from above:
TodoActions.spec.js
import expect from 'expect';
import proxyquire from 'proxyquire';
import * as types from '../../constants/ActionTypes';
const now = '2016-01-06T15:30:00-05:00';
const commonStub = {'getTimestamp': () => now};
const actions = proxyquire('../../actions/TodoActions', {
'../../utils/CommonUtils': commonStub
});
describe('actions', () => {
it('should create an action to add a todo', () => {
const text = 'Finish docs';
const timestamp = now; // <-- Use the variable defined above
const expectedAction = {
type: types.ADD_TODO,
text,
timestamp
};
expect(actions.addTodo(text)).toEqual(expectedAction);
});
});

Resources