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

Show both internal and shared state managment in example. #7312

Merged
merged 13 commits into from
May 23, 2019
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 27 additions & 0 deletions examples/with-reasonml/components/Context.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module type Config = {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

What does this file mean? It was quite difficult to explain this to a non-react user.

Choose a reason for hiding this comment

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

This was the example I found in the discord channel. As far as what it means, it would probably require some discussion on functors in Ocaml/ReasonML but I would argue this file is not as important as CountContext.re which provides the API for how to create a global value using the context API. I could always add comments to this file that explains things more.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes I agree that it will take some discussion of Functors, and module composition (because of the include). Also, this requires a lot of knowledge of React internals.

I think this is rather scary for someone new. I like this example because it does showcase some more advanced functionality. But perhaps it should be it’s own example? Removing the ref from with-reasonml and making the context example being with-reasonml-context ?

Choose a reason for hiding this comment

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

Yeah, this is fine having it in a separate example. I am not that attached like I said before. It's was definitely interesting exploring the context API and how to implement it here.

type context;
let defaultValue: context;
};

module Make = (Config: Config) => {
let x = React.createContext(Config.defaultValue);

module Provider = {
let make = x->React.Context.provider;

[@bs.obj]
external makeProps:
(
~value: Config.context,
~children: React.element,
~key: string=?,
unit
) =>
{
.
"value": Config.context,
"children": React.element,
} =
"";
};
};
13 changes: 13 additions & 0 deletions examples/with-reasonml/components/CountConsumer.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
type reducer = (CountContext.state, CountContext.action => unit);

[@react.component]
let make = () => {
let ({count}, dispatch): reducer = React.useContext(CountContext.x);

<div>
<div> {j|This is the count: $count|j}->React.string </div>
<button onClick={_e => dispatch(CountContext.Increment)}>
"Increment count"->React.string
</button>
</div>;
};
21 changes: 21 additions & 0 deletions examples/with-reasonml/components/CountContext.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
type state = {count: int};
type action =
| Increment;
let init = {count: 0};
let reducer = (state, action) => {
switch (action) {
| Increment => {count: state.count + 1}
};
};

type t = (state, action => unit);

include Context.Make({
type context = t;
let defaultValue = (
init,
_ => {
();
},
);
});
6 changes: 6 additions & 0 deletions examples/with-reasonml/components/CountProvider.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[@react.component]
let make = (~children) => {
let reducer = React.useReducer(CountContext.reducer, CountContext.init);

<CountContext.Provider value=reducer> children </CountContext.Provider>;
};
40 changes: 28 additions & 12 deletions examples/with-reasonml/components/Counter.re
Original file line number Diff line number Diff line change
@@ -1,22 +1,38 @@
let count = ref(0);
/*
This is the set of action messages which are produced by this component.
* Add updates the components internal state.
*/
type action =
| Add;

/*
This is the components internal state representation. This state object
is unique to each instance of the component.
*/
type state = {count: int};

let counterReducer = (state, action) =>
switch (action) {
| Add => {count: state.count + 1}
};

[@react.component]
let make = () => {
let (_state, dispatch) = React.useReducer(
(_, _) => Js.Obj.empty(),
Js.Obj.empty()
);
let (state, dispatch) = React.useReducer(counterReducer, {count: 0});
let (globalState, globalDispatch) = GlobalCount.useGlobalCount();

let countMsg = "Count: " ++ string_of_int(count^);

let add = () => {
count := count^ + 1;
dispatch();
};
let countMsg = "Count: " ++ string_of_int(state.count);
let persistentCountMsg = "Persistent Count " ++ string_of_int(globalState);

<div>
<p> {ReasonReact.string(countMsg)} </p>
<button onClick={_ => add()}> {React.string("Add")} </button>
<button onClick={_ => dispatch(Add)}> {React.string("Add")} </button>
<p> {ReasonReact.string(persistentCountMsg)} </p>
<button onClick={_ => globalDispatch(GlobalCount.Increment)}>
{React.string("Add")}
</button>
<CountConsumer />
<CountConsumer />
</div>;
};

Expand Down
27 changes: 27 additions & 0 deletions examples/with-reasonml/components/GlobalCount.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
## Global Count
This captures the count globally so that it will be persisted across
the `Index` and `About` pages. This replicates the functionality
of the shared-modules example.
*/
type t = ref(int);

type action =
| Increment;

let current = ref(0);

let increment = () => {
current := current^ + 1;
current;
};

let reducer = (_state, action) => {
switch(action) {
| Increment =>
let updated = increment();
updated^
}
};

let useGlobalCount = () => React.useReducer(reducer, current^);
28 changes: 28 additions & 0 deletions examples/with-reasonml/pages/_app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import App, { Container } from 'next/app'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was the file I was trying to avoid adding.

Choose a reason for hiding this comment

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

If you play around with next for any considerable amount of time you will probably use this file at some point. I don't see a reason to avoid it when it's provided to allow you to do this very thing.

import React from 'react'
import { make as CountProvider } from '../components/CountProvider.bs'

class MyApp extends App {
static async getInitialProps({ Component, router, ctx }) {
let pageProps = {}

if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}

return { pageProps }
}

render() {
const { Component, pageProps, store } = this.props
return (
<Container>
<CountProvider>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I feel like this wrapping is exactly what the Hooks API is trying to avoid. Is there another way we can inject the provider?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also, we're injecting context in a place where it takes quite a leap to find.

Choose a reason for hiding this comment

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

I don't think there is another way to inject the provider if we want it to work on every page without having to prepend it to the top level of index.re and about.re. I see this as a constraint of next being a JS framework and so have to work with its nuance.

As far as wrapping and how it relates to hooks, you still need a Provider, so I don't think there is a way to get around wrapping in terms of the context API. Maybe in the new version of reductive this will be solved.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It seems like a lot of ceremony to avoid the use of a ref. I think the ref can be avoided if your willing to simply replace the state on each update.

Also, now we are introducing many more concepts, steps and indirections.

I get that you may have to use this file eventually, but is it necessary in this case? Should it be something that should be avoided unless really necessary? To me, it’s akin to using bs.raw

Choose a reason for hiding this comment

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

There is definitely more boilerplate here and I don't really like that at all. At the same time, you can make the same comments about redux. But underneath all the boilerplate are some good principles. Not that what we have here is great but just like with redux you learn the patterns and you move on. So I kinda see it from that perspective.

<Component {...pageProps} />
</CountProvider>
</Container>
)
}
}

export default MyApp