Giter Club home page Giter Club logo

fetchye's Introduction

Fetchye - One Amex

If you know how to use Fetch, you know how to use Fetchye [fetch-yae]. Simple React Hooks, Centralized Cache, Infinitely Extensible.

// ...
import { useFetchye } from 'fetchye';

const MyComponent = () => {
  const { isLoading, data } = useFetchye('http://example.com/api/profile');
  return (
    <>
      {!isLoading && (
        <p>{data.body.name}</p>
      )}
    </>
  );
};

📖 Table of Contents

✨ Features

  • ES6 Fetch powered by React Hooks
  • Pure React or Redux Shared Cache modes available
  • Headless per-Hook Cache Mode available
  • SSR-friendly

⬇️ Install & Setup

Contents

Quick Install

💡 Makes use of Headless per-Hook Cache Mode

Pros

  • Painless and Quick

Cons

  • No shared caching
  • No de-duplication of API calls

Just install and do useFetchye. Thats it!

npm i -S fetchye
// ...
import { useFetchye } from 'fetchye';

const MyComponent = () => {
  const { isLoading, data } = useFetchye('http://example.com/api/profile');
  return (
    <>
      {!isLoading && (
        <p>{data?.body.name}</p>
      )}
    </>
  );
};

🏖️Try this out on Codesandbox

FetchyeProvider Install

💡 When you want a central cache but no extra dependencies

Pros

  • Easy
  • Shared Cache
  • De-duplication of API calls

Cons

  • No Redux Dev Tools for debugging and cache inspection
  • Limited centralized server-side data hydration support

Install fetchye:

npm i -S fetchye

Add the <FetchyeProvider /> component:

import { FetchyeProvider } from 'fetchye';

const ParentComponent = ({ children }) => (
  <>
    <FetchyeProvider>
      {/* Use your Router to supply children components containing useFetchye */}
      {children}
    </FetchyeProvider>
  </>
);

In a child React Component, do useFetchye queries:

// ...
import { useFetchye } from 'fetchye';

const MyComponent = () => {
  const { isLoading, data } = useFetchye('http://example.com/api/profile');
  return (
    <>
      {!isLoading && (
        <p>{data?.body.name}</p>
      )}
    </>
  );
};

🏖️Try this out on Codesandbox

FetchyeReduxProvider Install

💡 When you want a central cache integrated with a Redux based project

Pros

  • Easy if you know Redux
  • Shared Cache
  • De-duplication of API calls
  • Redux Dev Tools for debugging and cache inspection
  • Excellent centralized server-side data hydration support

Cons

  • More steps and dependencies

Add fetchye and its needed optional dependencies:

npm i -S fetchye redux react-redux

Add the <FetchyeReduxProvider /> component under the Redux <Provider />:

import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { FetchyeReduxProvider } from 'fetchye-redux-provider';
import { SimpleCache } from 'fetchye';

const fetchyeCache = SimpleCache({
  // Need to tell Fetchye where the cache reducer will be located
  cacheSelector: (state) => state,
});
const store = createStore(fetchyeCache.reducer);

const ParentComponent = ({ children }) => (
  <>
    <Provider store={store}>
      <FetchyeReduxProvider cache={fetchyeCache}>
        {/* Use your Router to supply children components containing useFetchye */}
        {children}
      </FetchyeReduxProvider>
    </Provider>
  </>
);

In a child React Component, do useFetchye queries:

// ...
import { useFetchye } from 'fetchye';

const MyComponent = () => {
  const { isLoading, data } = useFetchye('http://example.com/api/profile');
  return (
    <>
      {!isLoading && (
        <p>{data.body.name}</p>
      )}
    </>
  );
};

🏖️Try this out on Codesandbox

One App Install

💡 For when you use the One App Micro-Frontend Framework

Pros

  • Shared Cache
  • De-duplication of API calls
  • Redux Dev Tools for debugging and cache inspection
  • Excellent centralized server-side data hydration support
  • Shared Cache between Micro Frontend Holocron Modules
  • Immutable Redux State
  • Minimal configuration

Cons

  • More steps and dependencies
npm i -S fetchye fetchye-one-app

fetchye-one-app provides pre-configured provider, cache, makeOneServerFetchye and oneCacheSelector to ensure that all modules use the same cache and reduce the chance for cache misses. These all have restricted APIs to reduce the chance for misconfiguration however if you require more control/customization use ImmutableCache, FetchyeReduxProvider and makeServerFetchye. Please bear in mind that this can impact modules which are do not use the same configuration.

Add the <OneFetchyeProvider /> component from fetchye-one-app to your Root Holocron Module, and add the reducer from OneCache scoped under fetchye:

// ...
import { combineReducers } from 'redux-immutable';
import { OneFetchyeProvider, OneCache } from 'fetchye-one-app';

const MyModuleRoot = ({ children }) => (
  <>
    { /* OneFetchyeProvider is configured to use OneCache */ }
    <OneFetchyeProvider>
      {/* Use your Router to supply children components containing useFetchye */}
      {children}
    </OneFetchyeProvider>
  </>
);

// ...

MyModuleRoot.holocron = {
  name: 'my-module-root',
  reducer: combineReducers({
    // ensure you scope the reducer under "fetchye", this is important
    // to ensure that child modules can make use of the single cache
    fetchye: OneCache().reducer,
    // ... other reducers
  }),
};

In a child React Component or Holocron Module, do useFetchye queries:

// ...
import { useFetchye } from 'fetchye';

const MyComponent = () => {
  const { isLoading, data } = useFetchye('http://example.com/api/profile');
  return (
    <>
      {!isLoading && (
        <p>{data?.body.name}</p>
      )}
    </>
  );
};

This minimal configuration works as the provider, cache and makeOneServerFetchye, mentioned later, all follow expected conventions.

🤹‍ Usage

Real-World Example

import React from 'react';
import { useFetchye } from 'fetchye';

const BookList = ({ genre }) => {
  const { isLoading, data } = useFetchye(`http://example.com/api/books/?genre=${genre}`, {
    headers: {
      'X-Some-Tracer-Id': 1234,
    },
  });

  if (isLoading) {
    return (<p>Loading...</p>);
  }

  return (
    <>
      {data.status === 200 && (
        <>
          <h1>Books by {genre}</h1>
          <ul>
            {data.body.map((book) => (
              <li key={book.id}>{book.title} by {book.author}</li>
            ))}
          </ul>
        </>
      )}
      {data.status !== 200 && (
        <p>Oops!</p>
      )}
    </>
  );
};

Deferred execution

When you need to delay execution of a useFetchye call, you may use { defer: true } option. This is great for forms:

import React from 'react';
import { useFetchye } from 'fetchye';

const NewBookForm = () => {
  const { formData, onChange } = useSomeFormHandler();
  const { isLoading, run } = useFetchye('http://example.com/api/books', {
    // include defer here
    defer: true,
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      ...formData,
    }),
  });

  const onSubmit = async (event) => {
    event.preventDefault();
    const { data, error } = await run();
    // Check to make sure data.status === 200 for success
  };

  return (
    <form onSubmit={onSubmit}>
      <>
        {/* ...form elements using onChange */}
      </>
      {/* Hide Submit button when sending POST request */}
      {!isLoading && <button type="submit">Submit</button>}
    </form>
  );
};

Sequential API Execution

import React from 'react';
import { useFetchye } from 'fetchye';

const MyFavoriteBook = () => {
  const { isLoading: loadingProfile, data: profile } = useFetchye('http://example.com/api/profile');
  const { isLoading: loadingBook, data: favoriteBook } = useFetchye(() => `http://example.com/api/books/${profile.body.favoriteBookId}`);

  if (loadingProfile) {
    return (<p>Loading...</p>);
  }
  if (profile.status !== 200) {
    return (<p>Oops!</p>);
  }
  if (loadingBook) {
    return (<p>Loading...</p>);
  }

  return (
    <>
      {favoriteBook.status === 200 && (
        <>
          <h1>My Favorite Book</h1>
          <h2>{favoriteBook.body.title}</h2>
        </>
      )}
      {favoriteBook.status !== 200 && (
        <p>Oops!</p>
      )}
    </>
  );
};

Refreshing

import React from 'react';
import { useFetchye } from 'fetchye';

const BookList = ({ genre }) => {
  const { isLoading, data: booksData, run } = useFetchye(`http://example.com/api/books/?genre=${genre}`);

  if (isLoading) {
    return (<p>Loading...</p>);
  }

  return (
    <>
      {/* Render booksData */}
      <button type="button" onClick={() => run()}>Refresh</button>
    </>
  );
};

Custom Fetcher

Custom fetchers allow for creating reusable data fetching logic for specific APIs or custom needs. They allow for a centrally provided fetchClient which wraps that client on a per useFetchye request basis.

import React from 'react';
import { useFetchye } from 'fetchye';

const graphqlFetcher = async (fetchClient, key, options) => {
  let res;
  let payload;
  let error;
  try {
    res = await fetchClient('https://example.com/graphql', {
      ...options,
      method: 'POST',
      headers: {
        'X-Correlation-Id': 12345,
        'Content-Type': 'application/json',
      },
      // The key contains the GraphQL object request rather than a URL in this case
      body: JSON.stringify(key),
    });
    // GraphQL Response
    const { data, errors } = await res.json();
    // Pass through GraphQL Data
    payload = {
      data,
      ok: res.ok,
      status: res.status,
    };
    // Assign GraphQL errors to error
    error = errors;
  } catch (requestError) {
    // eslint-disable-next-line no-console
    console.error(requestError);
    error = requestError;
  }
  return {
    payload,
    error,
  };
};

const BookList = ({ genre }) => {
  const { isLoading, data: booksData, run } = useFetchye({
    query: `
      query BookList($genre: Genre) {
        book(genre: $genre) {
          title
          author
        }
      }
      `,
    variables: { genre },
  }, {}, graphqlFetcher);

  if (isLoading) {
    return (<p>Loading...</p>);
  }

  return (
    <>
      {/* Render booksData */}
      <button type="button" onClick={() => run()}>Refresh</button>
    </>
  );
};

SSR

One App SSR

Using makeOneServerFetchye from fetchye-one-app ensures that the cache will always be configured correctly.

import React from 'react';
import { useFetchye } from 'fetchye';
import { makeOneServerFetchye } from 'fetchye-one-app';

const BookList = () => {
  const { isLoading, data } = useFetchye('http://example.com/api/books/');

  if (isLoading) {
    return (<p>Loading...</p>);
  }

  return (
    <>
      {/* Render data */}
    </>
  );
};

BookList.holocron = {
  loadModuleData: async ({ store: { dispatch, getState }, fetchClient }) => {
    if (global.BROWSER) {
      return;
    }
    const fetchye = makeOneServerFetchye({
      // Redux store
      store: { dispatch, getState },
      fetchClient,
    });

    // async/await fetchye has same arguments as useFetchye
    // dispatches events into the server side Redux store
    await fetchye('http://example.com/api/books/');
  },
};

export default BookList;

Next.JS SSR

import { useFetchye, makeServerFetchye, SimpleCache } from 'fetchye';

const cache = SimpleCache();

// Codesandbox takes a second to get Next.JS started...
export default function IndexPage({ initialBookList }) {
  const { isLoading, data } = useFetchye('http://example.com/api/books/', {
    initialData: initialBookList,
  });

  if (isLoading) {
    return (<p>Loading...</p>);
  }

  return (
    <>
      {/* Render data */}
    </>
  );
}

const fetchye = makeServerFetchye({
  cache,
  fetchClient: fetch,
});

export async function getServerSideProps() {
  try {
    // returns { data, error } payload for feeding initialData on client side
    const res = await fetchye('http://example.com/api/books/');
    return {
      props: {
        initialBookList: res,
      },
    };
  } catch (error) {
    console.error(error.message);
    return {};
  }
}

🏖️Try this out on Codesandbox

Write your own Cache

💬 Note: This is for advanced users with special cases. Advanced users should understand Redux design pattern concepts about Reducers and Actions before continuing.

Sometimes, the basic opinions of the cache may not be enough for a project's use case. We can create a very basic new Cache configuration like so:

import {
  IS_LOADING,
  SET_DATA,
  DELETE_DATA,
  ERROR,
  CLEAR_ERROR,
} from 'fetchye';

export function CustomCache({ cacheSelector = (state) => state }) {
  return {
    // The reducer will save each action to state by hash key
    reducer: (state, action) => {
      switch (action.type) {
        case IS_LOADING: {
          // return is loading state change
          return state;
        }
        case SET_DATA: {
          // return set data state change
          return state;
        }
        case DELETE_DATA: {
          // return delete data state change
          return state;
        }
        case ERROR: {
          // return error state change
          return state;
        }
        case CLEAR_ERROR: {
          // return clear error state change
          return state;
        }
        default: {
          return state;
        }
      }
    },
    getCacheByKey: (state, key) => ({
      data, // dig into state and return data by key hash value
      error, // dig into state and return error by key hash value
      loading, // dig into state and return loading by key hash value
    }),
    cacheSelector, // pass through optional cacheSelector property
  };
}

Next we may add this to one of the Fetchye Providers and we are done:

import { FetchyeProvider } from 'fetchye';
import { CustomCache } from './CustomCache';

const ParentComponent = ({ children }) => (
  <>
    <FetchyeProvider cache={CustomCache()}>
      {children}
    </FetchyeProvider>
  </>
);

💡 Check out Actions API docs and SimpleCache.js source file. These will give you insights into the actions used in the reducer and practical examples on a working Cache configuration.

🎛️ API

Contents

useFetchye

A React Hook used for dispatching asynchronous API requests.

Shape

const { isLoading, data, error, run } = useFetchye(key, { defer: Boolean, mapOptionsToKey: options => options, ...fetchOptions }, fetcher);

Arguments

name type required description
key String or () => String true A string or function returning a string that factors into cache key creation. Defaults to URL compatible string.
options Object<Options> false Options to pass through to ES6 Fetch. See Options table for the exceptions to this rule. The options object factors into cache key creation.
fetcher async (fetchClient: Fetch, key: String, options: Options) => ({ payload: Object, error?: Object }) false The async function that calls fetchClient by key and options. Returns a payload with outcome of fetchClient and an optional error object.

Options

name type required description
mapOptionsToKey (options: Options) => transformedOptions false A function that maps options to the key that will become part of the cache key
defer Boolean false Prevents execution of useFetchye on each render in favor of using the returned run function. Defaults to false
initialData Object false Seeds the initial data on first render of useFetchye to accomodate server side rendering Defaults to undefined
...restOptions ES6FetchOptions true Contains any ES6 Compatible fetch option. (See Fetch Options)

Returns

name type description
isLoading Boolean A boolean to indicate whether in loading state or not.
data Object A result of a fetchClient query. Defaults to returning { status, body, ok, headers } from fetchClient response
error? Object An object containing an error if present. Defaults to an Error object with a thrown fetch error. This is not for API errors (e.g. Status 500 or 400). See data for that
run async () => {} A function for bypassing the cache and firing an API call. This will cause isLoading === true and update the cache based on the result.

makeServerFetchye

A factory function used to generate an async/await fetchye function used for server-side API calls.

Shape

const fetchye = makeServerFetchye({ cache, fetchClient });

const { data, error } = await fetchye(key, options, fetcher);

makeServerFetchye Arguments

name type required description
cache Cache true Fetchye Cache object.
fetchClient ES6Fetch true A Fetch API compatible function.

fetchye Arguments

name type required description
key String or () => String true A string or function returning a string that factors into cache key creation. Defaults to URL compatible string.
options ES6FetchOptions false Options to pass through to ES6 Fetch.
fetcher async (fetchClient: Fetch, key: String, options: Options) => ({ payload: Object, error?: Object }) false The async function that calls fetchClient by key and options. Returns a payload with outcome of fetchClient and an optional error object.

fetchye Returns

name type description
data Object A result of a fetchClient query. Defaults to returning { status, body, ok, headers } from fetchClient response
error? Object An object containing an error if present. Defaults to an Error object with a thrown fetch error. This is not for API errors (e.g. Status 500 or 400). See data for that

makeOneServerFetchye

A factory function used to generate an async/await fetchye function used for making One App server-side API calls.

Shape

const fetchye = makeOneServerFetchye({ store, fetchClient });

const { data, error } = await fetchye(key, options, fetcher);

makeServerFetchye Arguments

name type required description
cache Cache false Defaults to OneCache Fetchye Cache object.
fetchClient ES6Fetch true A Fetch API compatible function.
store Store true A Redux Store

fetchye Arguments

name type required description
key String or () => String true A string or function returning a string that factors into cache key creation. Defaults to URL compatible string.
options ES6FetchOptions false Options to pass through to ES6 Fetch.
fetcher async (fetchClient: Fetch, key: String, options: Options) => ({ payload: Object, error?: Object }) false The async function that calls fetchClient by key and options. Returns a payload with outcome of fetchClient and an optional error object.

fetchye Returns

name type description
data Object A result of a fetchClient query. Defaults to returning { status, body, ok, headers } from fetchClient response
error? Object An object containing an error if present. Defaults to an Error object with a thrown fetch error. This is not for API errors (e.g. Status 500 or 400). See data for that

Providers

A Provider creates a React Context to connect all the useFetchye Hooks into a centrally stored cache.

FetchyeProvider

A React Context Provider that holds the centralized cache for all the useFetchye React Hooks' query data. This Provider uses useReducer for cache storage.

Shape

<FetchyeProvider cache={SimpleCache()}>
  {children}
</FetchyeProvider>

Props

name type required description
fetchClient ES6Fetch true A Fetch API compatible function.
cache Cache false Fetchye Cache object. Defaults to SimpleCache
initialData Object false Initial state to feed into Cache Configuration reducer

FetchyeReduxProvider

💡Requires additional redux and react-redux packages installed

A React Context Provider that uses Redux to store the centralized cache for all the useFetchye React Hooks' query data.

Shape

import { FetchyeReduxProvider } from "fetchye-redux-provider";
<Provider>
  <FetchyeReduxProvider cache={SimpleCache()}>
    {children}
  </FetchyeReduxProvider>
</Provider>

Context

name type required description
ReactReduxContext ReactReduxContext true A Redux Context from a <Provider />.

Props

name type required description
fetchClient ES6Fetch true A Fetch API compatible function.
cache Cache false Fetchye Cache object. Defaults to SimpleCache

OneFetchyeProvider

💡Requires additional redux and react-redux packages installed

A Context Provider that is specifically designed for use with One App.

Shape

import { OneFetchyeProvider } from 'fetchye-one-app';
<Provider>
  <OneFetchyeProvider>
    {children}
  </OneFetchyeProvider>
</Provider>;

Context

name type required description
ReactReduxContext ReactReduxContext true A Redux Context from a <Provider />.

Props

name type required description
fetchClient ES6Fetch true A Fetch API compatible function.
cache Cache false Fetchye Cache object. Defaults to OneCache

Caches

A factory function that returns a configuration object, used to instruct a Provider on how to store and retrieve fetchye cache data.

SimpleCache

This Cache configuration relies on plain old Javascript data structures to back the reducer and getCacheByKey functions.

Shape

import { SimpleCache } from 'fetchye';

const cache = SimpleCache({
  cacheSelector,
});

Arguments

name type required description
cacheSelector (state) => state false Required if using FetchyeReduxProvider A function that returns the location inside Redux State to the Fetchye Cache. (See Redux Selectors).

Returns

name type description
reducer (state, action) => state A function that reduces the next state of Fetchye Cache. (See Redux Reducers).
getCacheByKey (cache, key) => state A function that returns a minimum of { data, loading, error } for a specific cache key from cache state.
cacheSelector? (state) => state An optionally returned parameter. This function returns the location inside Redux State to the Fetchye Cache. (See Redux Selectors).

ImmutableCache

💡Requires additional immutable package installed

This Cache configuration relies on ImmutableJS data structures to back the reducer and getCacheByKey functions.

Shape

import { ImmutableCache } from 'fetchye-immutable-cache';

const cache = ImmutableCache({
  cacheSelector
});

Arguments

name type required description
cacheSelector (state) => state false Required if using FetchyeReduxProvider A function that returns the location inside Redux State to the Fetchye Cache. (See Redux Selectors).

Returns

name type description
reducer (state, action) => state A function that reduces the next state of Fetchye Cache. (See Redux Reducers).
getCacheByKey (cache = Immutable.Map(), key) => state A function that returns a minimum of { data, loading, error } for a specific cache key from cache state.
cacheSelector? (state) => state An optionally returned parameter. This function returns the location inside Redux State to the Fetchye Cache. (See Redux Selectors).

OneCache

💡Requires additional immutable package installed

This Cache configuration is specifically designed to work with One App, it relies on ImmutableJS data structures to back the reducer and getCacheByKey functions.

Shape

import { OneCache } from 'fetchye-one-app';

const cache = OneCache();

Returns

name type description
reducer (state, action) => state A function that reduces the next state of Fetchye Cache. (See Redux Reducers).
getCacheByKey (cache = Immutable.Map(), key) => state A function that returns a minimum of { data, loading, error } for a specific cache key from cache state.
cacheSelector? (state) => state An optionally returned parameter. This function returns the location inside Redux State to the Fetchye Cache. (See Redux Selectors).

Actions

These actions power the state transitions described in a Cache Configuration reducer function.

IS_LOADING

An event signaling a state transition to the loading state by hash key.

Shape

import { IS_LOADING } from 'fetchye-core';
{
  type: IS_LOADING,
  hash,
}

Child Properties

name type description
hash String The hash value generated by object-hash package for the query

SET_DATA

An event signaling a state transition inside a reducer to add or replace the data field and transition away from loading state by hash key.

Shape

import { SET_DATA } from 'fetchye-core';
{
  type: SET_DATA,
  hash,
  value,
}

Child Properties

name type description
hash String The hash value generated by object-hash package for the query
value Object Contains the payload data returned from the fetcher inside useFetchye

DELETE_DATA

An event signaling a state transition inside a reducer to remove the data field by hash key.

Shape

import { DELETE_DATA } from 'fetchye-core';
{
  type: DELETE_DATA,
  hash,
}

Child Properties

name type description
hash String The hash value generated by object-hash package for the query

ERROR

An event signaling a state transition inside a reducer to insert an error and transition away from loading state by hash key.

Shape

import { ERROR } from 'fetchye-core';
{
  type: ERROR,
  hash,
  error,
}

Child Properties

name type description
hash String The hash value generated by object-hash package for the query
error `Error String

CLEAR_ERROR

An event signaling a state transition inside a reducer to remove an error by hash key.

Shape

import { CLEAR_ERROR } from 'fetchye-core';
{
  type: CLEAR_ERROR,
  hash,
}

Child Properties

name type description
hash String The hash value generated by object-hash package for the query

📢 Mission

The Fetchye project wishes to bring a more flexible central caching experience using the best ideas of the Redux design pattern and options for the developer to choose how their data is stored. Fetchye provides React Context driven caching options backed by your choice of pure React (via useReducer) or Redux. Unlike many data fetching solutions, Fetchye Context Providers do not rely on singleton statics and can be instantiated throughout an application multiple times for multiple caches if so desired.

🏆 Contributing

We welcome Your interest in the American Express Open Source Community on Github. Any Contributor to any Open Source Project managed by the American Express Open Source Community must accept and sign an Agreement indicating agreement to the terms below. Except for the rights granted in this Agreement to American Express and to recipients of software distributed by American Express, You reserve all right, title, and interest, if any, in and to Your Contributions. Please fill out the Agreement.

Please feel free to open pull requests and see CONTRIBUTING.md to learn how to get started contributing.

🗝️ License

Any contributions made under this project will be governed by the Apache License 2.0.

🗣️ Code of Conduct

This project adheres to the American Express Community Guidelines. By participating, you are expected to honor these guidelines.

fetchye's People

Contributors

benmclees avatar jadshead avatar oneamexbot avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.