Github issues viewer
Let's use ConanJs to build a realistic app that browses a remote repository

Introduction

We used this example to experiment with defining different isolated states and show how they can be used together when different asynchronous calls are needed. In this section we have only shown the code in Typescript.

Creating the Conan states

We have divided the data handled by the app into four independent states:
The remote repository connection details is called repoState$:
1
export const repoState$: RepoState =
2
Conan.light<RepoData>('repo', {org: "rails", repo: "rails", page: 1})
Copied!
The specific repository data information retrieved is modelled in repoDetailsState$ :
1
export interface RepoDetailsData {
2
openIssuesCount: number
3
error: string | null
4
}
5
​
6
export const repoDetailsState$: RepoDetailsState = Conan.state<RepoDetailsData>({
7
name: 'repo-details',
8
initialData: {openIssuesCount: -1, error: null},
9
reducers: repoDetailsReducersFn,
10
actions: repoDetailsActionsFn
11
});
Copied!
The issues data is modelled in issuesState$:
1
export type IssuesState = ConanState<IssuesData, IssuesActions>;
2
​
3
export interface IssuesData {
4
issuesByNumber: Record<number, Issue>;
5
issues: Issue[];
6
issueId?: number;
7
displayType: 'issues' | 'comments';
8
}
9
​
10
export const issuesState$: IssuesState = Conan.state<IssuesData, IssuesReducers, IssuesActions>({
11
name: 'issues',
12
initialData: {
13
issuesByNumber: {} as Record<number, Issue>,
14
issues: [],
15
displayType: "issues"
16
},
17
reducers: issuesReducersFn,
18
actions: issueActionsFn
19
})
Copied!
and finally the issues comments is modelled in issuesCommentsState$:
1
export interface IssuesCommentsData {
2
commentsByIssue: Record<number, IssueComment[] | undefined>
3
}
4
​
5
export type IssuesCommentsState = ConanState<IssuesCommentsData, IssuesCommentsActions>;
6
​
7
export const issuesCommentsState$: IssuesCommentsState = Conan.state<IssuesCommentsData, IssuesCommentsReducersFn, IssuesCommentsActions>({
8
name: 'issues-comments',
9
initialData: {
10
commentsByIssue: {} as Record<number, IssueComment[]>,
11
},
12
reducers: issuesCommentsReducers,
13
actions: issueCommentsActionsFn
14
})
Copied!

Adding async operations

In this app we need to fetch the issues of a given repository, fetch the information of that same repository and finally retrieve the comments of a given issue. We have implemented this logic in a service called IssuesServiceImpl which uses ConanJs Asap to implement the asynchronous calls:
1
export interface IssuesService {
2
fetch(repo: string, org: string, page: number): Asap<Issue[]>;
3
​
4
fetchComments(commentsUrl: string): Asap<IssueComment[]>;
5
​
6
fetchRepoDetails(org: string, repo: string): Asap<RepoDetails>;
7
}
8
​
9
export class IssuesServiceImpl implements IssuesService {
10
fetch(repo: string, org: string, page: number = 1): Asap<Issue[]> {
11
return Asaps.fetch<Issue[]>(`https://api.github.com/repos/${org}/${repo}/issues?per_page=25&page=${page}`);
12
}
13
​
14
fetchComments(commentsUrl: string): Asap<IssueComment[]> {
15
return Asaps.fetch<IssueComment[]>(commentsUrl);
16
}
17
​
18
fetchRepoDetails(org: string, repo: string): Asap<RepoDetails> {
19
return Asaps.fetch(`https://api.github.com/repos/${org}/${repo}`);
20
}
21
}
Copied!

Adding DI dependencies

We have made these states and the service available to all the app via ConanJs DI:
1
interface AuxDependencies {
2
issuesService: IssuesService
3
}
4
​
5
export let diContext = DiContextFactory.createContext<App, AuxDependencies>(
6
{
7
issuesCommentsState: issuesCommentsState$,
8
issuesState: issuesState$,
9
repoState: repoState$,
10
repoDetailsState: repoDetailsState$
11
}, {
12
issuesService: IssuesServiceImpl
13
}
14
);
15
​
16
export interface App {
17
issuesState: IssuesState;
18
issuesCommentsState: IssuesCommentsState;
19
repoState: RepoState;
20
repoDetailsState: RepoDetailsState;
21
}
Copied!

Fetching the issues

We need to implement an async call to retrieve the issues of the repository given. We can see how that is done in the actions passed to issuesState$:
1
export interface IssuesActions {
2
fetch(repo: string, org: string, page: number): Asap<IssuesData>;
3
​
4
fetchIssue(issueId: number): IssuesData;
5
​
6
showIssues(): IssuesData;
7
}
8
​
9
​
10
export const issueActionsFn: ActionsFn<IssuesData, IssuesReducers, IssuesActions> = thread => ({
11
fetch(repo, org, page): Asap<IssuesData> {
12
return thread.monitor(
13
diContext.issuesService.fetch(repo, org, page).catch(() => thread.reducers.$fetch([])),
14
(issues, reducers) => reducers.$fetch(issues as Issue[]),
15
'fetch',
16
[repo, org, page]
17
)
18
},
19
fetchIssue(issueId: number): IssuesData {
20
return thread.reducers.$fetchIssue(issueId);
21
},
22
showIssues(): IssuesData {
23
return thread.reducers.$switchDisplay("issues");
24
}
25
})
Copied!
The fist action fetch has the async call, so it's wrapped with thread.monitor. The other 2 actions are synchronous, so they can just call the reducer
we can access the issuesService from anywhere, since it's defined in ConanJs DI

Fetching the repository information

We need another async call to retrieve the repository information, but this time bound to the repoDetailsState$. This state just has an action passed in, and it will look like:
1
export interface RepoDetailsActions {
2
fetchRepoDetails(repo, org): Asap<RepoDetailsData>;
3
}
4
​
5
export const repoDetailsActionsFn: ActionsFn<RepoDetailsData, RepoDetailsReducers, RepoDetailsActions> = thread => ({
6
fetchRepoDetails(repo, org): Asap<RepoDetailsData> {
7
return thread.monitor(
8
diContext.issuesService.fetchRepoDetails(repo, org).catch(() => thread.reducers.$fetchRepoDetails(-1, "error loading")),
9
(repoDetails, reducers) => reducers.$fetchRepoDetails(repoDetails.open_issues_count, ""),
10
'fetchRepoDetails',
11
[repo, org]
12
)
13
}
14
});
Copied!

Fetching comments

Our last async call will happen when the user selects to open an issue, and we need to show the comments. So the issuesCommentsState$ will have an action that looks like:
1
export interface IssuesCommentsActions {
2
fetchComments(issue: Issue): Asap<IssuesCommentsData>;
3
}
4
​
5
export const issueCommentsActionsFn: ActionsFn<IssuesCommentsData, IssuesCommentsReducersFn, IssuesCommentsActions> = thread => ({
6
fetchComments(issue: Issue): Asap<IssuesCommentsData> {
7
return thread.monitor(
8
diContext.issuesService.fetchComments(issue.comments_url).catch(() => thread.reducers.$fetch([])),
9
(comments, reducers) => reducers.$fetchComments(issue.id, comments as IssueComment[]),
10
'fetchComments',
11
issue.comments_url
12
)
13
}
14
})
Copied!

Displaying the issues pages

Let's now see how we can display the issues and the repository information. The following fragment belongs to the functional component IssuesListPage:
1
<div id="issue-list-page">
2
{repoDetailsState$.connectMap<HeaderProps>(
3
IssuesPageHeader,
4
data => ({
5
org: org,
6
repo: repo,
7
openIssuesCount: data.openIssuesCount
8
})
9
)
10
}
11
{diContext.issuesState.connect(IssuesList)}
12
</div>
Copied!
It uses connectMap to link repoDetailsState$ with the component IssuesPageHeader, and the DI context to fully connect issuesState with the component IssuesList.

Displaying the comments

The component IssueDetailsPage uses a ConanJs hook useConanState to connect with the state issuesCommentsState$ and retrieve the issues in its own useEffect:
1
const [commentsState] = useConanState<IssuesCommentsData, IssuesCommentsActions>(issuesCommentsState$);
2
​
3
useEffect(() => {
4
if (issue) {
5
fetchComments(issue)
6
}
7
}, []);
8
​
9
const comments = commentsState.commentsByIssue[issue.id];
10
let renderedComments;
11
if (comments) {
12
renderedComments = <IssueComments issue={issue} comments={comments}/>
13
}
Copied!

Getting the code

The full code for this example is available at
conan-js-examples/issues-viewer at master Β· conan-js/conan-js-examples
GitHub
or the code sandbox below, for more details on this ConanJs feature.

Code sandbox

​
​